pax_global_header00006660000000000000000000000064151104674140014515gustar00rootroot0000000000000052 comment=7aa8f7e5f5abaf306171223d34a4cf2f7e2065a6 flask-security-5.7.1/000077500000000000000000000000001511046741400144745ustar00rootroot00000000000000flask-security-5.7.1/.djlintrc000066400000000000000000000004741511046741400163130ustar00rootroot00000000000000{ "ignore": "H005,H006,H017,H025,H030,H031", "extension": "html", "indent": "2", "profile": "jinja", "format_attribute_template_tags": "true", "max_line_length": 120, "max_attribute_length": 240, "blank_line_after_tag": "from,endmacro", "blank_line_before_tag": "block,extends" } flask-security-5.7.1/.editorconfig000066400000000000000000000011111511046741400171430ustar00rootroot00000000000000# -*- coding: utf-8 -*- root = true [*] indent_style = space end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true charset = utf-8 # Python files [*.py] indent_size = 4 # isort plugin configuration known_first_party = flask_security multi_line_output = 2 default_section = THIRDPARTY # RST files (used by sphinx) [*.rst] indent_size = 4 # CSS, HTML, JS, JSON, YML [*.{css,html,js,json,yml}] indent_size = 2 # Matches the exact files either package.json or .travis.yml [{package.json,.travis.yml}] indent_size = 2 # Dockerfile [Dockerfile] indent_size = 4 flask-security-5.7.1/.git-blame-ignore-revs000066400000000000000000000001471511046741400205760ustar00rootroot00000000000000# Black 721d31e9cb02af22e3ad9d579b7b82123527fafe # Black 2024 af6e7176eb54e7e9de77dd6e2cba03630c13f14e flask-security-5.7.1/.github/000077500000000000000000000000001511046741400160345ustar00rootroot00000000000000flask-security-5.7.1/.github/dependabot.yml000066400000000000000000000005331511046741400206650ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: weekly groups: github-actions: patterns: - '*' - package-ecosystem: pip directory: /requirements/ schedule: interval: weekly groups: python-requirements: patterns: - '*' flask-security-5.7.1/.github/workflows/000077500000000000000000000000001511046741400200715ustar00rootroot00000000000000flask-security-5.7.1/.github/workflows/lock.yaml000066400000000000000000000012261511046741400217060ustar00rootroot00000000000000name: Lock inactive closed issues # Lock closed issues that have not received any further activity for two weeks. # This does not close open issues, only humans may do that. It is easier to # respond to new issues with fresh examples rather than continuing discussions # on old issues. on: schedule: - cron: '0 0 28 * *' permissions: issues: write pull-requests: write concurrency: group: lock jobs: lock: runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 with: issue-inactive-days: 14 pr-inactive-days: 14 discussion-inactive-days: 14 flask-security-5.7.1/.github/workflows/publish-too.yaml000066400000000000000000000041321511046741400232220ustar00rootroot00000000000000name: Publish-Too on: push: tags: - '*' jobs: build: runs-on: ubuntu-latest outputs: artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }} steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: '3.x' cache: pip cache-dependency-path: requirements*/*.txt # Use the commit date instead of the current date during the build. - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV - name: Create dist run: | python -m pip install -U pip pip install tox tox -e makedist-too - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 id: upload-artifact with: name: dist path: dist/ if-no-files-found: error create-release: needs: [build] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: artifact-ids: ${{ needs.build.outputs.artifact-id }} path: dist/ - name: create release run: gh release create --draft --repo ${{ github.repository }} ${{ github.ref_name }}-Too dist/* env: GH_TOKEN: ${{ github.token }} publish-pypi-too: needs: [build] # Wait for approval before attempting to upload to PyPI. This allows reviewing the # files in the draft release. environment: name: publish url: https://pypi.org/project/Flask-Security-Too/${{ github.ref_name }} runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: artifact-ids: ${{ needs.build.outputs.artifact-id }} path: dist/ - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: packages-dir: dist/ flask-security-5.7.1/.github/workflows/publish.yaml000066400000000000000000000041061511046741400224240ustar00rootroot00000000000000name: Publish on: push: tags: - '*' jobs: build: runs-on: ubuntu-latest outputs: artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }} steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: '3.x' cache: pip cache-dependency-path: requirements*/*.txt # Use the commit date instead of the current date during the build. - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV - name: Create dist run: | python -m pip install -U pip pip install tox tox -e makedist - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 id: upload-artifact with: name: dist path: dist/ if-no-files-found: error create-release: needs: [build] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: artifact-ids: ${{ needs.build.outputs.artifact-id }} path: dist/ - name: create release run: gh release create --draft --repo ${{ github.repository }} ${{ github.ref_name }} dist/* env: GH_TOKEN: ${{ github.token }} publish-pypi: needs: [build] # Wait for approval before attempting to upload to PyPI. This allows reviewing the # files in the draft release. environment: name: publish url: https://pypi.org/project/Flask-Security/${{ github.ref_name }} runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: artifact-ids: ${{ needs.build.outputs.artifact-id }} path: dist/ - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: packages-dir: dist/ flask-security-5.7.1/.github/workflows/tests.yml000066400000000000000000000074231511046741400217640ustar00rootroot00000000000000 name: tests on: push: branches: - main - "[0-9]+.[0-9]+.x" pull_request: branches: - main - "[0-9]+.[0-9]+.x" # Allows you to run this workflow manually from the Actions tab workflow_dispatch: jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - {python: '3.10', tox: 'py310-release'} - {python: '3.10', tox: 'py310-low'} - {python: '3.11', tox: 'py311-release'} - {python: '3.11', tox: 'py311-low'} - {python: '3.12', tox: 'py312-release' } - {python: '3.12', tox: 'py312-low' } - {python: '3.13', tox: 'py313-release' } - {python: 'pypy-3.10', tox: 'pypy310-release'} - {python: 'pypy-3.10', tox: 'pypy310-low'} steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} - name: update pip run: | python -m pip install -U pip - name: cache pip uses: actions/cache@v4 with: path: ~/.cache/pip key: pip-${{ runner.os }}-${{ matrix.tox }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('tox.ini') }}-${{ hashFiles('requirements/*.txt') }} - name: run tests run: | pip install tox tox -e ${{ matrix.tox }} lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: "3.12" - name: update pip run: | python -m pip install -U pip - name: Style, docs, mypy run: | pip install tox tox -e style,docs,mypy other: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: "3.12" - name: update pip run: | python -m pip install -U pip - name: nobabel, nowebauthn, noauthlib, noflasksqlalchemy, async run: | pip install tox tox -e nobabel,nowebauthn,noauthlib,noflasksqlalchemy,async cov: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: "3.12" - name: update pip run: | python -m pip install -U pip - name: Coverage run: | pip install tox coverage tox -e coverage - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: fail_ci_if_error: true verbose: true token: ${{ secrets.codecov_token }} realdb: runs-on: ubuntu-latest services: postgres: image: postgres:latest env: POSTGRES_USER: postgres POSTGRES_PASSWORD: testpw POSTGRES_DB: testdb ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: "3.12" - name: update pip run: | python -m pip install -U pip - name: cache pip uses: actions/cache@v4 with: path: ~/.cache/pip key: pip-${{ runner.os }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('tox.ini') }}-${{ hashFiles('requirements/*.txt') }} - name: Postgres run: | pip install tox tox -e realpostgres -- --realdburl='postgresql://postgres:testpw@localhost:5432/testdb' flask-security-5.7.1/.gitignore000066400000000000000000000012601511046741400164630ustar00rootroot00000000000000*.py[co] # Packages *.egg *.egg-info dist *build eggs parts bin var sdist develop-eggs .installed.cfg pip-wheel-metadata/ __pycache__/ # Installer logs pip-log.txt # Unit test / coverage reports .coverage* .tox .travis*requirements* coverage.xml #Translations *.mo #Mr Developer .mr.developer.cfg #Virtualenv env/ venv/ .venv/ #Editor temporaries *~ *.swp *.save *.db *cache/ # vim [._]*.s[a-w][a-z] [._]s[a-w][a-z] *.un~ Session.vim .netrwhist *~ .eggs/README.txt # Mac .DS_Store # Linux .directory # Pycharm files .idea/ # VScode .vscode/ ### Emacs template # -*- mode: gitignore; -*- # *~ **/*~ \#*\# /.emacs.desktop /.emacs.desktop.lock *.elc auto-save-list tramp .\#* flask-security-5.7.1/.pre-commit-config.yaml000066400000000000000000000020361511046741400207560ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks default_language_version: python: python3.12 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace - id: debug-statements - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: fix-byte-order-marker - repo: https://github.com/asottile/pyupgrade rev: v3.21.1 hooks: - id: pyupgrade args: [--py310-plus] - repo: https://github.com/psf/black rev: 25.11.0 hooks: - id: black - repo: https://github.com/pycqa/flake8 rev: 7.3.0 hooks: - id: flake8 additional_dependencies: - flake8-bugbear - flake8-implicit-str-concat - repo: https://github.com/Riverside-Healthcare/djLint rev: v1.36.4 hooks: - id: djlint-jinja files: "\\.html" types_or: ['html'] flask-security-5.7.1/.readthedocs.yml000066400000000000000000000011401511046741400175560ustar00rootroot00000000000000 # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py builder: html fail_on_warning: true # Optionally set the version of Python and requirements required to build your docs python: install: # Order might be important since requests needs idna <3 but our dependencies # install idna 3.1 - method: pip path: . - requirements: requirements/docs.txt flask-security-5.7.1/AUTHORS000066400000000000000000000014111511046741400155410ustar00rootroot00000000000000Flask-Security was written by Matt Wright and various contributors. Development Lead ```````````````` - Chris Wagner Maintainer `````````` - Chris Wagner Patches and Suggestions ``````````````````````` Alexander Sukharev Alexey Poryadin Andrew J. Camenga Anthony Plunkett Artem Andreev Catherine Wise Chris Haines Christophe Simonis David Ignacio Eric Butler Eskil Heyn Olsen Iuri de Silvio Jay Goel Jiri Kuncar Joe Esposito Joe Hand Josh Purvis Kostyantyn Leschenko Luca Invernizzi Manuel Ebert Martin Maillard Paweł Krześniak Robert Clark Rodrigue Cloutier Rotem Yaari Srijan Choudhary Tristan Escalada Vadim Kotov Walt Askew John Paraskevopoulos Chris Wagner Eric Regnier Gal Stainfeld Ivan Piskunov Tyler Baur Glenn Lehman flask-security-5.7.1/CHANGES.rst000066400000000000000000002671101511046741400163050ustar00rootroot00000000000000Flask-Security Changelog ======================== Here you can see the full list of changes between each Flask-Security release. Version 5.7.1 ------------- Released November 23, 2025 Fixes +++++ - (:issue:`1147`) Regression when updating hash algorithm from bcrypt (willcroft) Version 5.7.0 ------------- Released November 14, 2025 This release contains a set of small backward incompatible changes. Please read these notes carefully. Features & Improvements +++++++++++++++++++++++ - (:pr:`1132`) Add Arabic translations (samialfattani) - (:issue:`1123`) Enable forgot-password workflow for authenticated users. Fixes +++++ - (:pr:`1115`) Fix broken link in docs and improve docstrings/typing for util classes. - (:issue:`1127`) Add nonce to script tags if configured to support nonce-based Content-Security-Policy (ahanak). - (:issue:`1133`) Remove unnecessary (optional) dependency on sqlalchemy_utils. - (:pr:`1140`) Fix localization of tf_select choices. - (:pr:`1143`) Support bcrypt 5.0 - See below for important compatibility concerns. This also replaces passlib with libpass for all versions. Docs and Chores +++++++++++++++ - (:pr:`1144`) Update ES and IT translations (gissimo) - (:pr:`1106`) Drop support for Python 3.9. This removes the dependency on importlib_resources, updates pypy to 3.10, and uses 3.12 as base python for tests/tox. - (:pr:`1112`) Flip :py:data:`SECURITY_USE_REGISTER_V2` default to ``True``. - (:pr:`1117`) Flip default mail package back to Flask-Mail (from Flask-Mailman). - (:issue:`1139`) Change external facing terminology from 'WebAuthn Credential' to 'passkey'. - (:pr:`1142`) Setting of xx_util_cls from kwargs which was deprecated in 5.6.1 has been removed. The BACKWARDS_COMPAT_UNAUTHN option (code) which has been deprecated since 5.4 has been removed. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - Flask-Security now depends on ``libpass`` (https://pypi.org/project/libpass/) for all versions. Be sure to UNINSTALL passlib, ensure the passlib directory is empty and then install libpass - we have seen reports when both are installed - it doesn't work! In bcrypt 5.0 they started throwing a ValueError for passwords/secrets longer than 72 bytes. It is important to know that by default Flask-Security performs a double hash - taking the secret, using HMAC(SHA512) then b64encodng the result. This means that ANY password will be longer than 72 bytes (86 to be exact). In the past bcrypt would silently truncate the input - now we have to do that explicitly. OWASP says truncation concerns are negligible: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#input-limits-of-bcrypt - The default RegisterForm is now the new RegisterFormV2 - Please read :ref:`register_form_migration`. Flask-Security will emit a DeprecationWarning if the :py:data:`SECURITY_USE_REGISTER_V2` is set to False. - In 5.0 we changed the default mailer package to Flask-Mailman since Flask-Mail was no longer supported. Flask-Mail is again supported and is part of Pallets-Eco. Both packages are still supported based on which one an application initializes. The only backwards compatibility concern is that if you use the setup extras 'common', it will install Flask-Mail rather than Flask-Mailman. - In the optional dependencies 'fsqla' we removed sqlalchemy_utils - while many applications might want these useful add-ons - they aren't required for standard SQLAlchemy use. Version 5.6.2 ------------- Released May 4, 2025 Fixes +++++ - (:issue:`1032` and :issue:`1096`) Use libpass for python >= 3.12 - (:pr:`1086`) Fix FR translation test for Change Password (nickcuenca) - (:issue:`1090`) Properly document context variables available in email templates. - (:issue:`1093`) Add confirmation link/token and reset link/token to welcome_existing email template. Notes +++++ Since Python 3.12 no longer contains setuptools - the old passlib failed to import. Rather than require setuptools, for Python >=3.12 we now depend on the fork ``libpass`` (https://pypi.org/project/libpass/) This is a very new package and rather than possibly cause backwards compat issues for projects not using Python >=3.12 - Flask-Security maintains the dependency on passlib for Python <3.12. Note: you can still use passlib for 3.12 and 3.13 - you have to manually add setuptools. Version 5.6.1 ------------- Released March 18, 2025 Fixes +++++ - (:issue:`1077`) Fix runtime modification of a config string (TWO_FACTOR_METHODS) - (:issue:`1078`) Fix CLI user_create when model doesn't contain username - (:issue:`1076`) xxx_util_cls instances should be public and documented. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ As part of :issue:`1076` the following cleanup was done: - The xxx_util_cls arguments are now stored in 'private' instance variables - they are never used after Flask-Security initialization and have never been documented. - The xxx_util_cls options should only be set as part of Flask-Security construction. Setting them via init_app(kwargs) or app.config["SECURITY_XX"] has been deprecated. Version 5.6.0 ------------- Released February 12, 2025 Features & Improvements +++++++++++++++++++++++ - (:issue:`1038`) Add support for 'secret_key' rotation (jamesejr) - (:issue:`980`) Add support for username recovery in simple login flows (jamesejr) - (:issue:`1055`) Add support for changing username - (:pr:`1048`) Add support for Python 3.13 - (:issue:`1043`) Unify Register forms (and split out re-type password option) Please read :ref:`register_form_migration`. Fixes +++++ - (:pr:`1062`) Fix duplicate HTML ids in templates. - (:pr:`1067`) Fix more duplicate HTML ids in templates. - (:issue:`1064`) Ensure templates pass W3C validation (see below) Docs and Chores +++++++++++++++ - (:pr:`1052`) Remove deprecated TWO_FACTOR configuration variables - (:pr:`1069`) Update ES and IT translations (gissimo) - (:pr:`1071`) Improve templates - two-factor is hyphenated, re-authenticate is not. Also try to embed links into xlatable strings. Notes +++++ Python 3.13 removed ``crypt``, which passlib attempts to import and use as part of its safe_crypt() method (fallback is to return None). However - that method only appears to be called in a few crypt handlers and for bcrypt - only for the built-in bcrypt - not if the bcrypt package is installed. passlib is not maintained - a new fork (10/1/2024) (https://pypi.org/project/libpass/) seems promising and has been tested with python 3.13 and Flask-Security. If that fork matures we will change the dependencies appropriately. The register forms have been combined - or more accurately - there is a new RegisterFormV2 that subsumes the features of both the old RegisterForm and ConfirmRegisterForm. Please read :ref:`register_form_migration`. The SECURITY_TWO_FACTOR_{SECRET, URI_SERVICE_NAME, SMS_SERVICE, SMS_SERVICE_CONFIG} have been removed (they have been deprecated for a while). Use the equivalent :py:data:`SECURITY_TOTP_SECRETS`, :py:data:`SECURITY_TOTP_ISSUER`, :py:data:`SECURITY_SMS_SERVICE` and :py:data:`SECURITY_SMS_SERVICE_CONFIG`. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ The fixes to all the templates to pass W3C validation could introduce some incompatibilities: - All templates now have a default - before, the <title> element was empty. - The HTML id of the rescue form submit button was changed to 'rescue' - The HTML id of the webauthn delete form name field was changed to 'delete-name' - Some template headings were changed to improve consistency - The csrf_token HTML id was changed on us_setup.html, wan_register.html, two_factor_setup.html two_factor_verify_code.html, us_verify.html, verify.html for the second form on the page. - On us_setup.html and two_factor_setup.html the submit code button HTML id was changed. Version 5.5.2 ------------- Released August 5, 2024 More attempts to upload to pypi both flask-security and flask-security-too. No code changes - however the build manifest changed so the source distribution contents might be slightly different. Docs and Chores +++++++++++++++ - (:pr:`1019`) Separate publish workflows for each pypi package Version 5.5.1 ------------- Released August 1, 2024 I am pleased to announce that Flask-Security-Too is now part of pallets-eco and has returned to be released as 'Flask-Security'. For the foreseeable future, we will publish the same release to both Flask-Security and Flask-Security-Too on PyPI. There are no code changes. Docs and Chores +++++++++++++++ - (:pr:`1015`) Convert docs, links, badges, etc to pallets-eco Version 5.5.0 ------------- Released July 24, 2024 Features & Improvements +++++++++++++++++++++++ - (:issue:`956`) Add support for changing registered user's email (:py:data:`SECURITY_CHANGE_EMAIL`). - (:issue:`944`) Change default password hash to argon2 (was bcrypt). See below for details. - (:pr:`990`) Add freshness capability to auth tokens (enables /us-setup to function w/ just auth tokens). - (:pr:`991`) Add support to /tf-setup to not require sessions (use a state token). - (:issue:`994`) Add support for Flask-SQLAlchemy-Lite - including new all-inclusive models that conform to sqlalchemy latest best-practice (type-annotated). - (:pr:`1007`) Convert other sqlalchemy-based datastores from legacy 'model.query' to best-practice 'select' - (:issue:`983`) Allow applications more flexibility defining allowable redirects. Fixes +++++ - (:pr:`972`) Set :py:data:`SECURITY_CSRF_COOKIE` at beginning (GET /login) of authentication ritual - just as we return the CSRF token. (thanks @e-goto) - (:issue:`973`) login and unified sign in should handle GET for authenticated user consistently. - (:pr:`995`) Don't show sms options if not defined in US_ENABLED_METHODS. (fredipevcin) - (:pr:`1009`) Change :py:data:`SECURITY_DEPRECATED_HASHING_SCHEMES` to ``["auto"]``. Docs and Chores +++++++++++++++ - (:pr:`979`) Update Russian translations (ademaro) - (:pr:`1004`) Update ES and IT translations (gissimo) - (:pr:`981` and :pr:`977`) Improve docs - (:pr:`992`) The long deprecated `get_token_status` is no longer exported - (:pr:`992`) Drop Python 3.8 support. - (:issue:`1001`) Try a different approach to typing User and Role models. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - Notes around the change to argon2 as the default password hash: - applications should add the argon2_cffi package to their requirements (it is included in the flask_security[common] extras). - leave bcrypt installed so that old passwords still work. - the default configuration will re-hash passwords with argon2 upon first use. - Changes to /tf-setup The old path - using state set in the session still works as before. The new path is just for the case an authenticated user wants to change their 2FA setup. - Changes to sqlalchemy-based datastores Flask-Security no longer uses the legacy model.query - all DB access is done via `select(xx).where(xx)`. As a result the find_user() method now only takes a SINGLE column:value from its kwargs - in prior releases all kwargs were passed into the query.filter. Version 5.4.3 ------------- Released March 23, 2024 Fixes +++++ - (:issue:`950`) Regression - some templates no longer getting correct config (thanks pete7863). - (:issue:`954`) CSRF not properly ignored for application forms using :py:data:`SECURITY_CSRF_PROTECT_MECHANISMS`. - (:pr:`957`) Improve jp translations (e-goto) - (:issue:`959`) Regression - datetime_factory should still be an attribute (thanks TimotheeJeannin) - (:issue:`942`) :py:data:`SECURITY_RETURN_GENERIC_RESPONSES` hid email validation/syntax errors. Version 5.4.2 ------------- Released March 8, 2024 Fixes +++++ - (:issue:`946`) OpenAPI spec missing. - (:pr:`945`) Doc fixes (e-goto) - (:pr:`941`) Update ES/IT translations (gissimo) Version 5.4.0 & 5.4.1 ---------------------- Released February 26, 2024 Among other changes, this continues the process of dis-entangling Flask-Security from Flask-Login and may require some application changes due to backwards incompatible changes. Features & Improvements +++++++++++++++++++++++ - (:issue:`879`) Work with Flask[async]. view decorators and signals support async handlers. - (:pr:`900`) CI support for python 3.12 - (:pr:`901`) Work with py_webauthn 2.0 (and only 2.0+) - (:pr:`899`) Improve (and simplify) Two-Factor setup. See below for backwards compatability issues and new functionality. - (:issue:`912`) Improve oauth debugging support. Handle next propagation in a more general way. - (:pr:`877`) Make AnonymousUser (Flask-Login) optional and deprecated. - (:pr:`906`) Remove undocumented and untested looking in session for possible 'next' redirect location. - (:pr:`881`) No longer rely on Flask-Login.unauthorized callback. See below for implications. - (:issue:`904`) Changes to default unauthorized handler - remove use of referrer header (see below) and document precise behavior. - (:pr:`927`) The authentication_token format has changed - adding per-token expiry time and future session ID. Old tokens are still accepted. Docs and Chores +++++++++++++++ - (:pr:`889`) Improve method translations for unified signin and two factor. Remove support for Flask-Babelex. - (:pr:`911`) Chore - stop setting all config as attributes. init_app(\*\*kwargs) can only set forms, flags, and utility classes (see below for compatibility concerns). - (:pr:`873`) Update Spanish and Italian translations. (gissimo) - (:pr:`855`) Improve translations for two-factor method selection. (gissimo) - (:pr:`866`) Improve German translations. (sr-verde) - (:pr:`911`) Remove deprecation of AUTO_LOGIN_AFTER_CONFIRM - it has a reasonable use case. - (:pr:`931`) Update message extraction - note that the CONFIRM_REGISTRATION message was changed to improve readability. Fixes +++++ - (:issue:`845`) us-signin magic link should use fs_uniquifier (not email). - (:issue:`893`) Improve open-redirect vulnerability mitigation. (see below) - (:issue:`875`) user_datastore.create_user has side effects on mutable inputs. (NoRePercussions) - (:pr:`878`) The long deprecated _unauthorized_callback/handler has been removed. - (:issue:`884`) Oauth re-used POST_LOGIN_VIEW which caused confusion. See below for the new configuration and implications. - (:pr:`908`) Improve CSRF documentation and testing. Fix bug where a CSRF failure could return an HTML page even if the request was JSON. - (:issue:`925`) Register with JSON and authentication token failed CSRF. (lilz-egoto) - (:issue:`870`) Fix 2 issues with CSRF configuration. - (:pr:`914`) It was possible that if :data:`SECURITY_EMAIL_VALIDATOR_ARGS` were set that deliverability would be checked even for login. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - Passing in an AnonymousUser class as part of Security initialization has been removed. - The never-public method _get_unauthorized_response has been removed. - Social-Oauth - a new configuration variable :py:data:`SECURITY_POST_OAUTH_LOGIN_VIEW` was introduced and it replaces :py:data:`SECURITY_POST_LOGIN_VIEW` in the oauthresponse logic when :py:data:`SECURITY_REDIRECT_BEHAVIOR` == `"spa"`. - Two-Factor setup. Prior to this release when setting up "SMS" the `/tf-setup` endpoint could be POSTed to w/o a phone number, and then another POST could be made to set the phone number. This has always been confusing and added complexity to the code. Now, if "SMS" is selected, the phone number must be supplied (which has always been supported). Other changes: - The default two-factor-setup.html template now has a more generic `"Enter code to complete setup"` message. - Make sure the `"disable"` option first. - Adding any currently configured two-factor method on setup failure. - The two_factor_verify template won't show the rescue form if it isn't set. - A GET on /tf-validate now returns the two-factor-validate-form always - before if the client was validating a new method, it would return the two-factor-setup-form - After successfully disabling two-factor the client is redirected to :py:data:`SECURITY_TWO_FACTOR_POST_SETUP_VIEW` rather than :py:data:`SECURITY_POST_LOGIN_VIEW`. - Bring unauthenticated handling completely into Flask-Security: Prior to this release, Flask-Security's :meth:`.Security.unauthn_handler` - called when a request wasn't properly authenticated - handled JSON requests then delegated form responses to Flask-Login's unauthenticated_callback. That logic has been moved into Flask-Security and Flask-Login is configured to call back into Flask-Security's handler. While the logic is very similar the following differences might be observed: - Flask-Login's FORCE_HOST_FOR_REDIRECTS configuration isn't honored - Flask-Login's USE_SESSION_FOR_NEXT configuration isn't honored - The flashed message is SECURITY_MSG_UNAUTHENTICATED rather than SECURITY_MSG_LOGIN. Furthermore, SECURITY_MSG_UNAUTHENTICATED was reworded to read better. - Flask-Login uses `urlencode` to encode the `next` query param - which quotes the '/' character. Werkzeug (which Flask-Security uses to build the URL) uses `quote` which considers '/' a safe character and isn't encoded. - The signal sent on an unauthenticated request has changed to :data:`user_unauthenticated`. Flask-Login used to send a `user_unauthorized` signal. - Flask-Security no longer configures anything related to Flask-Login's `fresh_login` logic. This shouldn't be used - instead use Flask-Security's :meth:`flask_security.auth_required` decorator. - Support for Flask-Babelex has been removed. Please convert to Flask-Babel. - JSON error response has changed due to issue with WTForms form-level errors. When WTForms introduced form-level errors they added it to the form.errors response using `None` as a key. When serializing it, it would turn into "null". However, if there is more than one error the default settings for JSON serialization in Flask attempt to sort the keys - which fails with the `None` key. An issue has been filed with WTForms - and maybe it will be changed. Flask-Security now changes any `None` key to `""`. - The default unauthorized handler behavior has changed slightly and is now documented. The default (:data:`SECURITY_UNAUTHORIZED_VIEW` == ``None``) has not changed (a default HTTP 403 response). The precise behavior when :data:`SECURITY_UNAUTHORIZED_VIEW` is set was never documented. The important change is that Flask-Security no longer ever looks at the request.referrer header and will never redirect to it. If an application needs that, it can provide a callable that can return that or any other header. - Configuration variables (and other things) are no longer added as attributes on the Security instance. For example `security.username_enable` no longer exists - this could be an issue in code or templates. For templates, Flask places `config` in the Jinja context - so rather than using an attribute, use `config["SECURITY_USERNAME_ENABLE"]` for the example above. - Open Redirect mitigation. Release 4.1.0 had a fix for :issue:`486` involving a potential open redirect. This was very low priority since the default configuration of Werkzeug (always convert the Location header to absolute URL) rendered the vulnerability un-exploitable. The solution at that time was to add an optional regex looking for these bizarre URLs that from a HTTP spec perspective are relative, but various browsers would interpret as absolute. In Werkzeug release 2.1 the default was changed so that the Location header was allowed to be a relative URL. This made the open redirect vulnerability much more likely to be exploitable. More recently, additional bizarre URLs were found, as documented in :issue:`893`. More work was done and a patch release 5.3.3 was published. This fix utilized changing the Werkzeug default back to absolute and an updated regex. Comments and thoughts by @gmanfuncky proposed a much better solution and that is in 5.4. This implementation is independent of Werkzeug (and relative Location headers are again the default). The entire regex option has been removed. Instead, any user-supplied path used as a redirect is parsed and quoted. Notes ++++++ - Historically, the **current_user** proxy (managed by Flask-Login) always pointed to a user object. If the user wasn't authenticated, it pointed to an AnonymousUser object. With this release, setting :py:data:`SECURITY_ANONYMOUS_USER_DISABLED` to `True` will force **current_user** to be set to `None` if the requesting user isn't authenticated. It should be noted that this is in support of a proposal by the Pallets team to remove AnonymousUser from Flask-Login - as well as deprecating the `is_authenticated` property. The default behavior (`False`) should be the same as prior releases. A new function `_fs_is_user_authenticated` is now part of the render_template context that templates can use instead of `current_user.is_authenticated`. Version 5.3.3 ------------- Released December 29, 2023 Fixes +++++ - (:issue:`893`) Once again work on open-redirect vulnerability - this time due to newer Werkzeug. Addresses: CVE-2023-49438 Version 5.3.2 ------------- Released October 23, 2023 Fixes ++++++ - (:issue:`859`) Update Quickstart to show how to properly handle SQLAlchemy connections. - (:issue:`861`) Auth Token not returned from /tf-validate. (thanks lilz-egoto) - (:pr:`864`) Fix for latest email_validator deprecation - bump minimum to 2.0.0 - (:pr:`865`) Deprecate passing in the anonymous_user class (sent to Flask-Login). Version 5.3.1 ------------- Released October 14, 2023 **Please Note:** - If your application uses webauthn you must use pydantic < 2.0 until the issue with user_handle is resolved. Fixes ++++++ - (:issue:`847`) Compatability with Flask 3.0 (wangsha) - (:issue:`829`) Revert change in 5.3.0 that added a Referrer-Policy header. - (:issue:`826`) Fix error in quickstart (codycollier) - (:pr:`835`) Update Armenian translations (amkrtchyan-tmp) - (:pr:`831`) Update German translations. (sr-verde) - (:issue:`853`) Fix 'next' propagation when passed as form.next (thanks cariaso) Version 5.3.0 ------------- Released July 27, 2023 This is a minor version bump due to some small backwards incompatible changes to WebAuthn, recoverability (/reset), confirmation (/confirm) and the two factor validity feature. Fixes ++++++ - (:pr:`807`) Webauthn Updates to handling of transport. - (:pr:`809`) Fix MongoDB support by eliminating dependency on flask-mongoengine. Improve MongoDB quickstart. - (:issue:`801`) Fix Quickstart for SQLAlchemy with scoped session. - (:issue:`806`) Login no longer, by default, checks for email deliverability. - (:issue:`791`) Token authentication is no longer accepted on endpoints which only allow 'session' as authentication-method. (N247S) - (:issue:`814`) /reset and /confirm and GENERIC_RESPONSES and additional form args don't mix. - (:issue:`281`) Reset password can be exploited and other OWASP improvements. - (:pr:`817`) Confirmation can be exploited and other OWASP improvements. - (:pr:`819`) Convert to pyproject.toml, build, remove setup.py/.cfg. - (:pr:`823`) the tf_validity feature now ONLY sets a cookie - and the token is no longer returned as part of a JSON response. - (:pr:`825`) Fix login/unified signin templates to properly send CSRF token. Add more tests. - (:pr:`826`) Improve Social Oauth example code. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - To align with the W3C WebAuthn Level2 and 3 spec - transports are now part of the registration response. This has been changed BOTH in the server code (using webauthn data structures) as well as the sample javascript code. If an application has their own javascript front end code - it might need to be changed. - The tf_validity feature :py:data:`SECURITY_TWO_FACTOR_ALWAYS_VALIDATE` used to set a cookie if the request was form based, and return the token as part of a JSON response. Now, this feature is ONLY cookie based and the token is no longer returned as part of any response. - Reset password was changed to adhere to OWASP recommendations and reduce possible exploitation: - A new email (with new token) is no longer sent upon expired token. Users must restart the reset password process. - The user is no longer automatically logged in upon successful password reset. For backwards compatibility :py:data:`SECURITY_AUTO_LOGIN_AFTER_RESET` can be set to ``True``. Note that this compatibility feature is deprecated and will be removed in a future release. - Identity information (identity, email) is no longer sent as part of the URL redirect query params. - The SECURITY_MSG_PASSWORD_RESET_EXPIRED message no longer contains the user's identity/email. - The default for :py:data:`SECURITY_RESET_PASSWORD_WITHIN` has been changed from `5 days` to `1 days`. - The response to GET /reset/<token> sets the HTTP header `Referrer-Policy` to `no-referrer` as suggested by OWASP. *PLEASE NOTE: this was backed out in 5.3.1* - Confirm email was changed to adhere to OWASP recommendations and reduce possible exploitation: - A new email (with new token) is no longer sent upon expired token. Users must restart the confirmation process. - Identity information (identity, email) is no longer sent as part of the URL redirect query params. - The :py:data:`SECURITY_AUTO_LOGIN_AFTER_CONFIRM` configuration variable now defaults to ``False`` - meaning after a successful email confirmation, the user must still sign in using the usual mechanisms. This is to align better with OWASP best practices. Setting it to ``True`` will restore prior behavior. - The SECURITY_MSG_CONFIRMATION_EXPIRED message no longer contains the user's identity/email. - The response to GET /reset/<token> sets the HTTP header `Referrer-Policy` to `no-referrer` as suggested by OWASP. *PLEASE NOTE: this was backed out in 5.3.1* Version 5.2.0 ------------- Released May 6, 2023 Note: Due to rapid deprecation and removal of APIs from the Pallets team, maintaining the testing of back versions of various packages is taking too much time and effort. In this release only current versions of the various dependent packages are being tested. Fixes +++++ - (:issue:`764`) Remove old Werkzeug compatibility check. - (:issue:`777`) Compatibility with Quart. - (:pr:`780`) Remove dependence on pkg_resources / setuptools (use importlib_resources package) - (:pr:`792`) Fix tests to work with latest Werkzeug/Flask. Update requirements_low to match current releases. - (:pr:`792`) Drop support for Python 3.7 Known Issues ++++++++++++ - Flask-mongoengine hasn't released in a while and currently will not work with latest Flask and Flask-Security-Too/Flask-Security (this is due to the JSONEncoder being deprecated and removed). Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - The removal of pkg_resources required changing the config variable :py:data:`SECURITY_I18N_DIRNAME`. If your application modified or extended this configuration variable, a small change will be required. Version 5.1.2 ------------- Released March 12, 2023 Fixes +++++ - (:issue:`771`) Hungarian translations not working. - (:pr:`769`) Fix documentation for send_mail. (gg) - (:pr:`768`) Fix for latest mongoengine and mongomock. - (:pr:`766`) Fix inappropriate use of &thinsp& in French translations. (maxdup) - (:pr:`773`) Improve documentation around subclassing forms. Version 5.1.1 ------------- Released March 1, 2023 Fixes +++++ - (:issue:`740`) Fix 2 Flask apps in same thread with USERNAME_ENABLE set. There was a too aggressive config check. - (:pr:`739`) Update Russian translations. (ademaro) - (:pr:`743`) Run all templates through a linter. (ademaro) - (:pr:`757`) Fix json/flask backwards compatibility hack. - (:issue:`759`) Fix quickstarts - make sure they run using `flask run` - (:pr:`755`) Fix unified signup when two-factor not enabled. (sebdroid) - (:pr:`763`) Add dependency on setuptools (pkg_resources). (hroncok) Version 5.1.0 ------------- Released January 23, 2023 Features ++++++++ - (:issue:`667`) Expose form instantiation. See :ref:`form_instantiation`. - (:issue:`693`) Option to encrypt recovery codes. - (:pr:`716`) Support for authentication via 'social' oauth. - (:pr:`721`) Support for Python 3.11 Fixes +++++ - (:pr:`678`) Fixes for Flask-SQLAlchemy 3.0.0. (jrast) - (:pr:`680`) Fixes for sqlalchemy 2.0.0 (jrast) - (:issue:`697`) Webauthn and Unified signin features now properly take into account blueprint prefixes. - (:issue:`699`) Properly propagate `?next=/xx` - the verify, webauthn, and unified signin endpoints, that had multiple redirects, needed fixes. - (:pr:`696`) Add Hungarian translations. (xQwexx) - (:issue:`701`) Two factor redirects ignored url_prefix. Added a :py:data:`SECURITY_TWO_FACTOR_ERROR_VIEW` configuration option. - (:issue:`704`) Add configurations for static folder/URL and make sure templates reference blueprint relative static folder. - (:issue:`709`) Make (some) templates look better by using single quotes instead of double quotes. - (:issue:`690`) Send entire context to MailUtil::send_mail (patrickyan) - (:pr:`728`) Support for Flask-Babel 3.0.0 - (:issue:`692`) Add configuration option :py:data:`SECURITY_TWO_FACTOR_POST_SETUP_VIEW` which is redirected to upon successful change of a two factor method. - (:pr:`733`) The ability to pass in a LoginManager instance which was deprecated in 5.0 has been removed. - (:issue:`732`) If :py:data:`SECURITY_USERNAME_REQUIRED` was ``True`` then users couldn't login with just an email. - (:issue:`734`) If :py:data:`SECURITY_USERNAME_ENABLE` is set, bleach is a requirement. - (:pr:`736`) The unauthz_handler now takes a function name, not the function! Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - Each form class used to be set as an attribute on the Security object. With the new form instantiation model, they no longer are. - After a successful update/change of a two-factor method, the user was redirected to :py:data:`SECURITY_POST_LOGIN_VIEW`. Now it redirects to :py:data:`SECURITY_TWO_FACTOR_POST_SETUP_VIEW` which defaults to `".two_factor_setup"`. - The :meth:`.Security.unauthz_handler` now takes a function name - not the function - which never made sense. Version 5.0.2 ------------- Released September 23, 2022 Fixes +++++ - (:issue:`673`) Role permissions backwards compatibility bug. For SQL based datastores that use Flask-Security's models.fsqla_vx - there should be NO issues. If you declare your own models - please see the 5.0.0 releases notes for required change. Version 5.0.1 ------------- Released September 6, 2022 Fixes +++++ - (:pr:`662`) Fix Change Password regression. (tysonholub) Version 5.0.0 ------------- Released August 27, 2022 **PLEASE READ CHANGE NOTES CAREFULLY - THERE ARE LIKELY REQUIRED CHANGES YOU WILL HAVE TO MAKE.** Features ++++++++ - (:issue:`475`) Support for WebAuthn. - (:issue:`479`) Support Two-factor recovery codes. - (:issue:`585`) Provide option to prevent user enumeration (i.e. Generic Responses). - (:pr:`532`) Support for Python 3.10. - (:pr:`657`, :pr:`655`) Support for Flask >= 2.2. - (:pr:`540`) Improve Templates in support of JS required by WebAuthn. - (:pr:`608`) Add Icelandic translations. (ofurkusi) - (:pr:`650`) Update German translations. (sr-verde) - (:issue:`256`) Add custom HTML attributes to improve user experience. This changed LoginForm quite a bit - please see backwards compatability concerns below. The default LoginForm and template should be the same as before. - (:pr:`638`) The JSON errors response has been unified. Please see backwards compatibility concerns below. - Updated all-inclusive data models (fsqla_v3). Add fields necessary for the new WebAuthn and Two-Factor recovery codes features. Changed `us_phone_number` to be unique (but not required). Changed `password` to be nullable. Deprecations ++++++++++++ - (:pr:`568`) Deprecate the old passwordless feature in favor of Unified Signin. - (:pr:`568`) Deprecate replacing login_manager so we can possibly vendor that in in the future. - (:pr:`654`) The previously deprecated methods RoleMixin.add_permissions and RoleMixin.remove_permissions have been removed. - (:pr:`657`) The ability to pass in a json_encoder_cls as part of initialization has been removed since Flask 2.2 has deprecated and replaced that functionality. - (:pr:`655`) Flask has deprecated @before_first_request. This was used mostly in examples/quickstart. These have been changed to use app.app_context() prior to running the app. Flask-Security itself used it in 2 places - to populate `_` in jinja globals if Babel wasn't initialized and to perform various configuration sanity checks w.r.t. WTF CSRF. All Flask-Security templates have been converted to use `_fsdomain` rather than ``_`` so Flask-Security no longer sets ``_`` into jinja2 globals. The configuration checks have been moved to the end of Security::init_app() - so it is now imperative that `FlaskWTF::CSRFProtect()` be called PRIOR to initializing Flask-Security. - encrypt_password method has been removed. It has been deprecated since 2.0.2 - get_token_status has been deprecated. Fixes +++++ - (:pr:`591`) Make the required zxcvbn complexity score configurable. (mephi42) - (:issue:`531`) Get rid of Flask-Mail. Flask-Mailman is now the default preferred email package. Flask-Mail is still supported so there should be no backwards compatability issues. - (:issue:`597`) A delete option has been added to us-setup (form and view). - (:pr:`625`) Improve username support - the LoginForm now has a separate field for username if ``SECURITY_USERNAME_ENABLE`` is True, and properly displays input fields only if the associated field is an identity attribute (as specified by :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`). - (:pr:`627`) Improve empty password handling. Prior, an unguessable password was set into the user record when a user registered without a password - now, the DB user model has been changed to allow nullable passwords. This provides a better user experience since Flask-Security now knows if a user has an empty password or not. Since registering without a password is not a mainstream feature, a new configuration variable :py:data:`SECURITY_PASSWORD_REQUIRED` has been added (defaults to ``True``). - (:issue:`479`) A new configuration option :py:data:`SECURITY_TWO_FACTOR_RESCUE_EMAIL` has been added that allows disabling that feature - defaults to backwards compatible ``True`` - (:issue:`658`) us_phone_number needs to be validated to be unique. Backward Compatibility Concerns ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For unified signin: - The redirect after a successful us-setup used to redirect to ``SECURITY_US_POST_SETUP_VIEW`` or ``SECURITY_POST_LOGIN_VIEW`` (which would default to '/'). Now it just redirects to ``SECURITY_US_POST_SETUP_VIEW`` which defaults back to the ``/us-setup`` view. - The ability to authenticate using a one-time email link was automatically setup by the system for all users. "email" now behaves like the other unified sign in methods and must be explicitly set up - with the exception that if a user registers WITHOUT a password, the system will setup the one-time email link option - since otherwise the user would never be able to authenticate. - ``/us-signin/send-code`` didn't used to check if the user account required confirmation it just sent a code and the ``/us-signin`` endpoint did the confirmation check. Now ``send-code`` does the confirmation check and won't send a code unless the user is confirmed. - In ``us-verify`` the 'code_methods' item now lists just active/setup methods that generate a code not ALL possible methods that generate a code. - ``SECURITY_US_VERIFY_SEND_CODE_URL`` and ``SECURITY_US_SIGNIN_SEND_CODE_URL`` endpoints are now POST only. - Empty passwords were always permitted when ``SECURITY_UNIFIED_SIGNIN`` was enabled - now an additional configuration variable ``SECURITY_PASSWORD_REQUIRED`` must be set to False. - ``SECURITY_US_VERIFY_SEND_CODE_URL`` and ``SECURITY_US_SIGNIN_SEND_CODE_URL`` used to send ``code_sent`` to the template. Now they flash the ``SECURITY_MSG_CODE_HAS_BEEN_SENT`` message. - With the addition of being able to delete a previously setup up sign in method, the signal `us_profile_changed` arguments have changed. `method` is now `methods` and is a list, and a new argument `delete` is True if a sign in option was deleted. Login: - Since the beginning of time, the flask-security login form has accepted any input in the 'email' field, and used that to check if it corresponds to any field in ``SECURITY_USER_IDENTITY_ATTRIBUTES``. This has always been problematic and confusing - and with the addition of HTML attributes for various form fields - having a field with multiple possible inputs is no longer a viable user experience. This is no longer supported, and the LoginForm now declares the ``email`` field to be of type ``EmailField`` which requires a valid (after normalization) email address. The most common usage of this legacy feature was to allow an email or username - Flask-Security now has core support for a ``username`` option - see :py:data:`SECURITY_USERNAME_ENABLE`. Please see :ref:`custom_login_form` for an example of how to replicate the legacy behavior. - Some error messages have changed - ``USER_DOES_NOT_EXIST`` is now returned for any identity error including an empty value. Other: - A very old piece of code in registrable, would immediately commit to the DB when a new user was created. It is now consistent with all other views, and has the caller responsible for committing the transaction - usually by setting up a flask ``after_this_request`` action. This could affect an application that captured the registration signal and stored the ``user`` object for later use - this user object would likely be invalid after the request is finished. - Some fields have custom HTML attributes attached to them (e.g. autocomplete, type, etc). These are stored as part of the form in the ``render_kw`` attribute. This could cause some confusion if an app had its own templates and set different attributes. - The keys for "/tf-rescue" select options have changed to be more 'action' oriented: - `lost_device` -> `email` - `no_mail_access` -> `help` - JSON error responses. **THIS IS A BREAKING CHANGE**. In earlier releases, the JSON error response could have either a `error` key which was for rare cases where there was a single non-form related error, or an `errors` key which was a a dict as defined by WTForms. Now, the `errors` key will contain a list of (localized) messages - both non-form related as well as any form related. The key `field_errors` will contain the dict as specified by WTForms. Please note that starting with WTForms 3.0 form-level errors are supported and show up in the dict with the field name/key of "none". There are no changes to non-error related JSON responses. - Permissions **THIS IS A BREAKING CHANGE**. The Role Model now stores permissions as a list, and requires that the underlying DB ORM map that to a supported DB type. For SQLAlchemy, this is mapped to a comma separated string (as before). For SQLAlchemy DBs the underlying Column type (UnicodeText) didn't change so no data migration should be required. However, the ORM Column type did change and requires the following change to your model:: from flask_security import AsaList from sqlalchemy.ext.mutable import MutableList class Role(Base, RoleMixin): ... permissions = Column(MutableList.as_mutable(AsaList()), nullable=True) ... If your application makes use of Flask-Security's models.fsqla_vX classes - no changes are required. For Mongo, a ListField can be directly used. - CSRF - As mentioned above, it is now required that `FlaskWTF::CSRFProtect()`, if used, must be called PRIOR to initializing Flask-Security. - json_encoder_cls - As mentioned above - Flask-Security initialization no longer accepts overriding the json_encoder class. If this is required, update to Flask >=2.2 and implement Flask's JSONProvider interface. For templates: - Pretty much every template was modified to replace <p> with <div class=xx> to make styling possible and to make more complex forms more readable. - Many forms had places where things weren't properly localizable - that has (hopefully) been fixed. - The ``us_setup.html`` template was modified to add ability to delete an existing set up method. DB Migration ~~~~~~~~~~~~ To use the new WebAuthn feature a new table and two new columns in the User model are required. To ease updates - Flask-Security will automatically create a fs_webauthn_user_handle upon first use for existing users. If you are using Alembic the schema migration is easy:: op.add_column('user', sa.Column('fs_webauthn_user_handle', sa.String(length=64), nullable=True, unique=True)) If you want to allow for empty passwords as part of registration then set :py:data:`SECURITY_PASSWORD_REQUIRED` to ``False``. In addition you need to change your DB schema to allow the ``password`` field to be nullable. Version 4.1.5 ------------- Released July 28, 2022 Fixes +++++ - (:pr:`644`) Fix test and other failures with newer Flask-Login/Werkzeug versions. Version 4.1.4 ------------- Released April 19, 2022 Fixes +++++ - (:issue:`594`) Fix test failures with newer Flask versions. Version 4.1.3 ------------- Released March 2, 2022 Fixes +++++ - (:issue:`581`) Fix bug when attempting to disable register_blueprint. (halali) - (:pr:`539`) Fix example documentation re: generating localized messages. (kazuhei2) - (:pr:`546`) Make roles joinedload compatible with SQLAlchemy 2.0. (keats) - (:pr:`586`) Ship py.typed as part of package. - (:issue:`580`) Improve documentation around use of bleach and include in common install extra. Version 4.1.2 ------------- Released September 22, 2021 Fixes +++++ - (:issue:`526`) default_reauthn_handler doesn't honor SECURITY_URL_PREFIX - (:pr:`528`) Improve German translations (sr-verde) - (:pr:`527`) Fix two-factor sample code (djpnewton) Version 4.1.1 -------------- Released September 10, 2021 Fixes +++++ - (:issue:`518`) Fix corner case where Security object was being reused in tests. - (:issue:`512`) If USERNAME_ENABLE is set, change LoginForm field from EmailField to StringField. Also - dynamically add fields to Login and Registration forms rather than always having them - this made the RegistrationForm much simpler. - (:issue:`516`) Improved username feature handling solved issue of always requiring bleach. - (:issue:`513`) Improve documentation of default username validation. Version 4.1.0 ------------- Released July 23, 2021 Features ++++++++ - (:issue:`474`) Add public API and CLI command to change a user's password. - (:issue:`140`) Add type hints. Please note that many of the packages that flask-security depends on aren't typed yet - so there are likely errors in some of the types. - (:issue:`466`) Add first-class support for using username for signing in. Fixes +++++ - (:issue:`483`) 4.0 doesn't accept 3.4 authentication tokens. (kuba-lilz) - (:issue:`490`) Flask-Mail sender name can be a tuple. (hrishikeshrt) - (:issue:`486`) Possible open redirect vulnerability. - (:pr:`478`) Improve/update German translation. (sr-verde) - (:issue:`488`) Improve handling of Babel packages. - (:pr:`496`) Documentation improvements, distribution extras, fix single message override. - (:issue:`497`) Improve cookie handling and default ``samesite`` to ``Strict``. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - (:pr:`488`) In 4.0.0, with the addition of Flask-Babel support, Flask-Security enforced that if it could import either Flask-Babel or Flask-BabelEx, that those modules had been initialized as proper Flask extensions. Prior to 4.0.0, just Flask-BabelEx was supported - and that didn't require any explicit initialization. Flask-Babel DOES require explicit initialization. However for some applications that don't completely control their environment (such as system pre-installed versions of python) this caused applications that didn't even want translation services to fail on startup. With this release, Flask-Security still attempts to import one or the other package - however if those modules are NOT initialized, Flask-Security will simply ignore them and no translations will occur. - (:issue:`497`) The CSRF_COOKIE and TWO_FACTOR_VALIDITY cookie had their defaults changed to set ``samesite=Strict``. This follows the Flask-Security goal of making things more secure out-of-the-box. - (:issue:`140`) Type hinting. For the most part this of course has no runtime effects. However, this required a fairly major overhaul of how Flask-Security is initialized in order to provide valid types for the many constructor attributes. There are no known compatability concerns - however initialization used to convert all arguments into kwargs then add those as attributes and merge with application constants. That no longer happens and it is possible that some corner cases don't behave precisely as they did before. Version 4.0.1 ------------- Released April 2, 2021 Features ++++++++ Fixes +++++ - (:issue:`461`) 4.0 doesn't accept 3.4 authentication tokens. (kuba-lilz) - (:issue:`460`) 2-fa error: Failed to send code - improved documentation and debuggability. - (:issue:`454`) 2-fa error: TypeError - fixed documentation. - (:issue:`443`) Calling create user without any arguments - fixed underlying cause of translating form errors in the CLI. - (:issue:`442`) Email validation confusion - added documentation. - (:issue:`450`) Add documentation on how to override specific error messages. - (:pr:`439`) Don't install global-scope tests. (mgorny) - (:pr:`470`) Add note about updating DB using MySQL. (jugmac00) - (:pr:`468`) Fix documentation - uia_phone_number should be uia_phone_mapper. (dvrg) - (:pr:`457`) Improve chinese translations. (zxjlm) - (:pr:`453`) Improve basque and spanish translations. (mmozos) - (:pr:`448`) Add Afrikaans translations. (lonelyvikingmichael) - (:pr:`467`) Add Blinker as explicit dependency, improve/fix celery usage docs, dont require pyqrcode unless authenticator configured, improve SMS configuration variables documentation. Version 4.0.0 ------------- 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 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 `.RoleMixin.add_permissions` and `.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 <https://pypi.org/project/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 <https://pages.nist.gov/800-63-3/sp800-63b.html#sec5>`_ 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/pallets-eco/flask-security/issues/418 .. _login: https://github.com/pallets-eco/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 +++++ - (: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 2, 2020 Only change is to move repo to the Flask-Middleware github organization. Version 3.4.1 -------------- Released April 22, 2020 Fix a bunch of bugs in new unified sign in along with a couple other major issues. Fixed +++++ - (:issue:`298`) Alternative ID feature ran afoul of postgres/psycopg2 finickiness. - (:issue:`300`) JSON 401 responses had WWW-Authenticate Header attached - that caused browsers to pop up their own login/password form. Not what applications want. - (:issue:`280`) Allow admin/api to setup TFA (and unified sign in) out of band. Please see :meth:`.UserDatastore.tf_set`, :meth:`.UserDatastore.tf_reset`, :meth:`.UserDatastore.us_set`, :meth:`.UserDatastore.us_reset` and :meth:`.UserDatastore.reset_user_access`. - (:pr:`305`) We used form._errors which wasn't very pythonic, and it was removed in WTForms 2.3.0. - (:pr:`310`) WTForms 2.3.0 made email_validator optional - we need it. Version 3.4.0 ------------- Released March 31, 2020 Features ++++++++ - (:pr:`257`) Support a unified sign in feature. Please see :ref:`configuration:unified signin`. - (:pr:`265`) Add phone number validation class. This is used in both unified sign in as well as two-factor when using ``sms``. - (:pr:`274`) Add support for 'freshness' of caller's authentication. This permits endpoints to be additionally protected by ensuring a recent authentication. - (:issue:`99`, :issue:`195`) Support pluggable password validators. Provide a default validator that offers complexity and breached support. - (:issue:`266`) Provide interface to two-factor send_token so that applications can provide error mitigation. Defaults to returning errors if can't send the verification code. - (:pr:`247`) Updated all-inclusive data models (fsqlaV2). Add fields necessary for the new unified sign in feature and changed 'username' to be unique (but not required). - (:pr:`245`) Use fs_uniquifier as the default Flask-Login 'alternative token'. Basically this means that changing the fs_uniquifier will cause outstanding auth tokens, session and remember me cookies to be invalidated. So if an account gets compromised, an admin can easily stop access. Prior to this cookies were storing the 'id' which is the user's primary key - difficult to change! (kishi85) Fixed +++++ - (:issue:`273`) Don't allow reset password for accounts that are disabled. - (:issue:`282`) Add configuration that disallows GET for logout. Allowing GET can cause some denial of service issues. The default still allows GET for backwards compatibility. (kantorii) - (:issue:`258`) Reset password wasn't integrated into the two-factor feature and therefore two-factor auth could be bypassed. - (:issue:`254`) Allow lists and sets as underlying permissions. (pffs) - (:issue:`251`) Allow a registration form to have additional fields that aren't part of the user model that are just passed to the user_registered.send signal, where the application can perform arbitrary additional actions required during registration. (kuba-lilz) - (:issue:`249`) Add configuration to disable the 'role-joining' optimization for SQLAlchemy. (pffs) - (:issue:`238`) Fix more issues with atomically setting the new TOTP secret when setting up two-factor. (kishi85) - (:pr:`240`) Fix Quart Compatibility. (ristellise) - (:issue:`232`) CSRF Cookie not being set when using 'Remember Me' cookie to re-sign in. (kishi85) - (:issue:`229`) Two-factor enabled accounts didn't work with the Remember Me feature. (kishi85) As part of adding unified sign in, there were many similarities with two-factor. Some refactoring was done to unify naming, configuration variables etc. It should all be backwards compatible. - In TWO_FACTOR_ENABLED_METHODS "mail" was changed to "email". "mail" will still be honored if already stored in DB. Also "google_authenticator" is now just "authenticator". - TWO_FACTOR_SECRET, TWO_FACTOR_URI_SERVICE_NAME, TWO_FACTOR_SMS_SERVICE, and TWO_FACTOR_SMS_SERVICE_CONFIG have all been deprecated in favor of names that are the same for two-factor and unified sign in. Other changes with possible backwards compatibility issues: - ``/tf-setup`` never did any phone number validation. Now it does. - ``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 ------------- Released February 11, 2020 Minor changes required to work with latest released Werkzeug and Flask-Login. Version 3.3.2 ------------- Released December 7, 2019 - (:issue:`215`) Fixed 2FA totp secret regeneration bug (kishi85) - (:issue:`172`) Fixed 'next' redirect error in login view - (:issue:`221`) Fixed regressions in login view when already authenticated user again does a GET or POST. - (:issue:`219`) Added example code for unit testing FS protected routes. - (:issue:`223`) Integrated two-factor auth into registration and confirmation. Thanks to kuba-lilz and kishi85 for finding and providing detailed issue reports. In Flask-Security 3.3.0 the login view was changed to allow already authenticated users to access the view. Prior to 3.3.0, the login view was protected with @anonymous_user_required - so any access (via GET or POST) would simply redirect the user to the ``POST_LOGIN_VIEW``. With the 3.3.0 changes, both GET and POST behaved oddly. GET simply returned the login template, and POST attempted to log out the current user, and log in the new user. This was problematic since this couldn't possibly work with CSRF. The old behavior has been restored, with the subtle change that older Flask-Security releases did not look at "next" in the form or request for the redirect, and now, all redirects from the login view will honor "next". Version 3.3.1 ------------- Released November 16, 2019 - (:pr:`197`) Add `Quart <https://gitlab.com/pgjones/quart/>`_ compatibility (Ristellise) - (:pr:`194`) Add Python 3.8 support into CI (jdevera) - (:pr:`196`) Improve docs around Single Page Applications and React (acidjunk) - (:issue:`201`) fsqla model was added to __init__.py making Sqlalchemy a required package. That is wrong and has been removed. Applications must now explicitly import from ``flask_security.models`` - (:pr:`204`) Fix/improve examples and quickstart to show one MUST call hash_password() when creating users programmatically. Also show real SECRET_KEYs and PASSWORD_SALTs and how to generate them. - (:pr:`209`) Add argon2 as an allowable password hash. - (:pr:`210`) Improve integration with Flask-Admin. Actually - this PR improves localization support by adding a method ``_fsdomain`` to jinja2's global environment. Added documentation around localization. Version 3.3.0 ------------- Released September 26, 2019 **There are several default behavior changes that might break existing applications. Most have configuration variables that restore prior behavior**. **If you use Authentication Tokens (rather than session cookies) you MUST make a (small) change. Please see below for details.** - (:pr:`120`) Native support for Permissions as part of Roles. Endpoints can be protected via permissions that are evaluated based on role(s) that the user has. - (:issue:`126`, :issue:`93`, :issue:`96`) Revamp entire CSRF handling. This adds support for Single Page Applications and having CSRF protection for browser(session) authentication but ignored for token based authentication. Add extensive documentation about all the options. - (:issue:`156`) Token authentication is slow. Please see below for details on how to enable a new, fast implementation. - (:issue:`130`) Enable applications to provide their own :meth:`.render_json` method so that they can create unified API responses. - (:issue:`121`) 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 to show how to use these. Please see :ref:`responsetopic` for details. - Improve documentation for :meth:`.UserDatastore.create_user` to make clear that hashed password should be passed in. - Improve documentation for :class:`.UserDatastore` and :func:`.verify_and_update_password` to make clear that caller must commit changes to DB if using a session based datastore. - (:issue:`122`) Clarify when to use ``confirm_register_form`` rather than ``register_form``. - Fix bug in 2FA that didn't commit DB after using `verify_and_update_password`. - Fix bug(s) in UserDatastore where changes to user ``active`` flag weren't being added to DB. - (:issue:`127`) JSON response was failing due to LazyStrings in error response. - (:issue:`117`) Making a user inactive should stop all access immediately. - (:issue:`134`) Confirmation token can no longer be reused. Added *SECURITY_AUTO_LOGIN_AFTER_CONFIRM* option for applications that don't want the user to be automatically logged in after confirmation (defaults to True - existing behavior). - (:issue:`159`) The ``/register`` endpoint returned the Authentication Token even though confirmation was required. This was a huge security hole - it has been fixed. - (:issue:`160`) The 2FA totp_secret would be regenerated upon submission, making QRCode not work. (malware-watch) - (:issue:`166`) `default_render_json` uses ``flask.make_response`` and forces the Content-Type to JSON for generating the response (koekie) - (:issue:`166`) *SECURITY_MSG_UNAUTHENTICATED* added to the configuration. - (:pr:`168`) When using the @auth_required or @auth_token_required decorators, the token would be verified twice, and the DB would be queried twice for the user. Given how slow token verification is - this was a significant issue. That has been fixed. - (:issue:`84`) The :func:`.anonymous_user_required` was not JSON friendly - always performing a redirect. Now, if the request 'wants' a JSON response - it will receive a 400 with an error message defined by *SECURITY_MSG_ANONYMOUS_USER_REQUIRED*. - (:pr:`145`) Improve 2FA templates to that they can be localized. (taavie) - (:issue:`173`) *SECURITY_UNAUTHORIZED_VIEW* didn't accept a url (just an endpoint). All other view configurations did. That has been fixed. Possible compatibility issues +++++++++++++++++++++++++++++ - (:pr:`164`) In prior releases, the Authentication Token was returned as part of the JSON response to each successful call to `/login`, `/change`, or `/reset/{token}` API call. This is not a great idea since for browser-based UIs that used JSON request/response, and used session based authentication - they would be sent this token - even though it was likely ignored. Since these tokens by default have no expiration time this exposed a needless security hole. The new default behavior is to ONLY return the Authentication Token from those APIs if the query param ``include_auth_token`` is added to the request. Prior behavior can be restored by setting the *SECURITY_BACKWARDS_COMPAT_AUTH_TOKEN* configuration variable. - (:pr:`120`) :class:`.RoleMixin` now has a method :meth:`.get_permissions` which is called as part each request to add Permissions to the authenticated user. It checks if the RoleModel has a property ``permissions`` and assumes it is a comma separated string of permissions. If your model already has such a property this will likely fail. You need to override :meth:`.get_permissions` and simply return an emtpy set. - (:issue:`121`) Changes the default (failure) behavior for views protected with @auth_required, @token_auth_required, or @http_auth_required. Before, a 401 was returned with some stock html. Now, Flask-Login.unauthorized() is called (the same as @login_required does) - which by default redirects to a login page/view. If you had provided your own `.Security.unauthorized_callback` there are no changes - that will still be called first. The old default behavior can be restored by setting *SECURITY_BACKWARDS_COMPAT_UNAUTHN* to True. Please see :ref:`responsetopic` for details. - (:issue:`127`) Fix for LazyStrings in json error response. The fix for this has Flask-Security registering its own JsonEncoder on its blueprint. If you registered your own JsonEncoder for your app - it will no longer be called when serializing responses to Flask-Security endpoints. You can register your JsonEncoder on Flask-Security's blueprint by sending it as `json_encoder_cls` as part of initialization. Be aware that your JsonEncoder needs to handle LazyStrings (see speaklater). - (:issue:`84`) Prior to this fix - anytime the decorator :func:`.anonymous_user_required` failed, it caused a redirect to the post_login_view. Now, if the caller wanted a JSON response, it will return a 400. - (:issue:`156`) Faster Authentication Token introduced the following non-backwards compatible behavior change: * Since the old Authentication Token algorithm used the (hashed) user's password, those tokens would be invalidated whenever the user changed their password. This is not likely to be what most users expect. Since the new Authentication Token algorithm doesn't refer to the user's password, changing the user's password won't invalidate outstanding Authentication Tokens. The method :meth:`.UserDatastore.set_uniquifier` can be used by an administrator to change a user's ``fs_uniquifier`` - but nothing the user themselves can do to invalidate their Authentication Tokens. Setting the *SECURITY_BACKWARDS_COMPAT_AUTH_TOKEN_INVALIDATE* configuration variable will cause the user's ``fs_uniquifier`` to be changed when they change their password, thus restoring prior behavior. New fast authentication token implementation ++++++++++++++++++++++++++++++++++++++++++++ Current auth tokens are slow because they use the user's password (hashed) as a uniquifier (the user id isn't really enough since it might be reused). This requires checking the (hashed) password against what is in the token on EVERY request - however hashing is (on purpose) slow. So this can add almost a whole second to every request. To solve this, a new attribute in the User model was added - ``fs_uniquifier``. If this is present in your User model, then it will be used instead of the password for ensuring the token corresponds to the correct user. This is very fast. If that attribute is NOT present - then the behavior falls back to the existing (slow) method. DB Migration ~~~~~~~~~~~~ To use the new UserModel mixins or to add the column ``user.fs_uniquifier`` to speed up token authentication, a schema AND data migration needs to happen. If you are using Alembic the schema migration is easy - but you need to add ``fs_uniquifier`` values to all your existing data. You can add code like this to your migrations::update method:: # be sure to MODIFY this line to make nullable=True: op.add_column('user', sa.Column('fs_uniquifier', sa.String(length=64), nullable=True)) # update existing rows with unique fs_uniquifier import uuid user_table = sa.Table('user', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True), sa.Column('fs_uniquifier', sa.String)) conn = op.get_bind() for row in conn.execute(sa.select([user_table.c.id])): conn.execute(user_table.update().values(fs_uniquifier=uuid.uuid4().hex).where(user_table.c.id == row['id'])) # finally - set nullable to false op.alter_column('user', 'fs_uniquifier', nullable=False) # for MySQL the previous line has to be replaced with... # op.alter_column('user', 'fs_uniquifier', existing_type=sa.String(length=64), nullable=False) Version 3.2.0 ------------- Released June 26th 2019 - (:pr:`80`) Support caching of authentication token (eregnier `opr #839 <https://github.com/mattupstate/flask-security/pull/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 as opposed to session. - (:pr:`81`) Support for JSON/Single-Page-Application. This completes support for non-form based access to Flask-Security. See PR for details. (jwag956) - (:pr:`79` Add POST logout to enhance JSON usage (jwag956). - (:pr:`73`) Fix get_user for various DBs (jwag956). This is a more complete fix than in opr #633. - (:pr:`78`, :pr:`103`) Add formal openapi API spec (jwag956). - (:pr:`86`, :pr:`94`, :pr:`98`, :pr:`101`, :pr:`104`) Add Two-factor authentication (opr #842) (baurt, jwag956). - (:issue:`108`) Fix form field label translations (jwag956) - (:issue:`115`) Fix form error message translations (upstream #801) (jwag956) - (:issue:`87`) Convert entire repo to Black (baurt) Version 3.1.0 ------------- Released never - (:pr:`53`) Use Security.render_template in mails too (noirbizarre `opr #487 <https://github.com/mattupstate/flask-security/pull/487>`_) - (:pr:`56`) Optimize DB accesses by using an SQL JOIN when retrieving a user. (nfvs `opr #679 <https://github.com/mattupstate/flask-security/pull/679>`_) - (:pr:`57`) Add base template to security templates (grihabor `opr #697 <https://github.com/mattupstate/flask-security/pull/697>`_) - (:pr:`73`) datastore: get user by numeric identity attribute (jirikuncar `opr #633 <https://github.com/mattupstate/flask-security/pull/633>`_) - (:pr:`58`) bugfix: support application factory pattern (briancappello `opr #703 <https://github.com/mattupstate/flask-security/pull/703>`_) - (:pr:`60`) Make SECURITY_PASSWORD_SINGLE_HASH a list of scheme ignoring double hash (noirbizarre `opr #714 <https://github.com/mattupstate/flask-security/pull/714>`_) - (:pr:`61`) Allow custom login_manager to be passed in to Flask-Security (jaza `opr #717 <https://github.com/mattupstate/flask-security/pull/717>`_) - (:pr:`62`) Docs for OAauth2-based custom login manager (jaza `opr #727 <https://github.com/mattupstate/flask-security/pull/727>`_) - (:pr:`63`) core: make the User model check the password (mklassen `opr #779 <https://github.com/mattupstate/flask-security/pull/779>`_) - (:pr:`64`) Customizable send_mail (abulte `opr #730 <https://github.com/mattupstate/flask-security/pull/730>`_) - (:pr:`68`) core: fix default for UNAUTHORIZED_VIEW (jirijunkar `opr #726 <https://github.com/mattupstate/flask-security/pull/726>`_) These should all be backwards compatible. Possible compatibility issues: - #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). - #679 - SqlAlchemy SQL improvement. It is possible you will get the following error:: Got exception during processing: <class 'sqlalchemy.exc.InvalidRequestError'> - 'User.roles' does not support object population - eager loading cannot be applied. This is likely solvable by removing ``lazy='dynamic'`` from your Role definition. Performance improvements: - #679 - for sqlalchemy, for each request, there would be 2 DB accesses - now there is one. Testing: For datastores operations, Sqlalchemy, peewee, pony were all tested against sqlite, postgres, and mysql real databases. Version 3.0.2 ------------- Released April 30th 2019 - (opr #439) HTTP Auth respects SECURITY_USER_IDENTITY_ATTRIBUTES (pnpnpn) - (opr #660) csrf_enabled` deprecation fix (abulte) - (opr #671) Fix referrer loop in _get_unauthorized_view(). (nfvs) - (opr #675) Fix AttributeError in _request_loader (sbagan) - (opr #676) Fix timing attack on login form (cript0nauta) - (opr #683) Close db connection after running tests (reambus) - (opr #691) docs: add password salt to SQLAlchemy app example (KshitijKarthick) - (opr #692) utils: fix incorrect email sender type (switowski) - (opr #696) Fixed broken Click link (williamhatcher) - (opr #722) Fix password recovery confirmation on deleted user (kesara) - (opr #747) Update login_user.html (rickwest) - (opr #748) i18n: configurable the dirname domain (escudero) - (opr #835) adds relevant user to reset password form for validation purposes (fuhrysteve) These are bug fixes and a couple very small additions. No change in behavior and no new functionality. 'opr#' is the original pull request from https://github.com/mattupstate/flask-security Version 3.0.1 -------------- Released April 28th 2019 - Support 3.7 as part of CI - Rebrand to this forked repo - (#15) Build docs and translations as part of CI - (#17) Move to msgcheck from pytest-translations - (opr #669) Fix for Read the Docs (jirikuncar) - (opr #710) Spanish translation (maukoquiroga) - (opr #712) i18n: improvements of German translations (eseifert) - (opr #713) i18n: add Portuguese (Brazilian) translation (dinorox) - (opr #719) docs: fix anchor links and typos (kesara) - (opr #751) i18n: fix missing space (abulte) - (opr #762) docs: fixed proxy import (lsmith) - (opr #767) Update customizing.rst (allanice001) - (opr #776) i18n: add Portuguese (Portugal) translation (micael-grilo) - (opr #791) Fix documentation for mattupstate#781 (fmerges) - (opr #796) Chinese translations (Steinkuo) - (opr #808) Clarify that a commit is needed after login_user (christophertull) - (opr #823) Add Turkish translation (Admicos) - (opr #831) Catalan translation (miceno) These are all documentation and i18n changes - NO code changes. All except the last 3 were accepted and reviewed by the original Flask-Security team. Thanks as always to all the contributors. Version 3.0.0 ------------- Released May 29th 2017 - Fixed a bug when user clicking confirmation link after confirmation and expiration causes confirmation email to resend. (see #556) - Added support for I18N. - Added options `SECURITY_EMAIL_PLAINTEXT` and `SECURITY_EMAIL_HTML` for sending 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. - Added port of CLI using Click configurable using options `SECURITY_CLI_USERS_NAME` and `SECURITY_CLI_ROLES_NAME`. - Added new configuration option `SECURITY_DATETIME_FACTORY` which can be used to force default timezone for newly created datetimes. (see mattupstate/flask-security#466) - Better IP tracking if using Flask 0.12. - Renamed deprecated Flask-WFT base form class. - Added tests for custom forms configured using app config. - Added validation and tests for next argument in logout endpoint. (see #499) - Bumped minimal required versions of several packages. - Extended test matric on Travis CI for minimal and released package versions. - Added of .editorconfig and forced tests for code style. - Fixed a security bug when validating a confirmation token, also checks if the email that the token was created with matches the user's current email. - Replaced token loader with request loader. - Changed trackable behavior of `login_user` when IP can not be detected from a request from 'untrackable' to `None` value. - Use ProxyFix instead of inspecting X-Forwarded-For header. - Fix identical problem with app as with datastore. - Removed always-failing assertion. - Fixed failure of init_app to set self.datastore. - Changed to new style flask imports. - Added proper error code when returning JSON response. - Changed 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`. - Inspect value of `SECURITY_UNAUTHORIZED_VIEW`. - Send password reset instructions if an attempt has expired. - Added "Forgot password?" link to LoginForm description. - Upgraded passlib, and removed bcrypt version restriction. - Removed a duplicate line ('retype_password': 'Retype Password') in forms.py. - Various documentation improvement. Version 1.7.5 ------------- Released December 2nd 2015 - Added `SECURITY_TOKEN_MAX_AGE` configuration setting - Fixed calls to `SQLAlchemyUserDatastore.get_user(None)` (this now returns `False` instead of raising a `TypeError` - Fixed URL generation adding extra slashes in some cases (see GitHub #343) - Fixed handling of trackable IP addresses when the `X-Forwarded-For` header contains multiple values - Include WWW-Authenticate headers in `@auth_required` authentication checks - Fixed error when `check_token` function is used with a json list - Added support for custom `AnonymousUser` classes - Restricted `forgot_password` endpoint to anonymous users - Allowed unauthorized callback to be overridden - Fixed issue where passwords cannot be reset if currently set to `None` - Ensured that password reset tokens are invalidated after use - Updated `is_authenticated` and `is_active` functions to support Flask-Login changes - Various documentation improvements Version 1.7.4 ------------- Released October 13th 2014 - Fixed a bug related to changing existing passwords from plaintext to hashed - Fixed a bug in form validation that did not enforce case insensitivity - Fixed a bug with validating redirects Version 1.7.3 ------------- Released June 10th 2014 - Fixed a bug where redirection to `SECURITY_POST_LOGIN_VIEW` was not respected - Fixed string encoding in various places to be friendly to unicode - Now using `werkzeug.security.safe_str_cmp` to check tokens - Removed user information from JSON output on `/reset` responses - Added Python 3.4 support Version 1.7.2 ------------- Released May 6th 2014 - Updated IP tracking to check for `X-Forwarded-For` header - Fixed a bug regarding the re-hashing of passwords with a new algorithm - Fixed a bug regarding the `password_changed` signal. Version 1.7.1 ------------- Released January 14th 2014 - Fixed a bug where passwords would fail to verify when specifying a password hash algorithm Version 1.7.0 ------------- Released January 10th 2014 - Python 3.3 support! - Dependency updates - Fixed a bug when `SECURITY_LOGIN_WITHOUT_CONFIRMATION = True` did not allow users to log in - Added `SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL` 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 - Added documentation for select functions in the `flask_security.utils` module - Fixed a bug in `flask_security.forms.NextFormMixin` - Added `CHANGE_PASSWORD_TEMPLATE` configuration option to optionally specify a different change password template - Added the ability to specify addtional fields on the user model to be used for identifying the user via the `USER_IDENTITY_ATTRIBUTES` configuration option - An error is now shown if a user tries to change their password and the password is the same as before. The message can be customed with the `SECURITY_MSG_PASSWORD_IS_SAME` configuration option - Fixed a bug in `MongoEngineUserDatastore` where user model would not be updated when using the `add_role_to_user` method - Added `SECURITY_SEND_PASSWORD_CHANGE_EMAIL` configuration option to optionally disable password change email from being sent - Fixed a bug in the `find_or_create_role` method of the PeeWee datastore - Removed pypy tests - Fixed some tests - Include CHANGES and LICENSE in MANIFEST.in - A bit of documentation cleanup - A bit of code cleanup including removal of unnecessary utcnow call and simplification of get_max_age method Version 1.6.9 ------------- Released August 20th 2013 - Fix bug in SQLAlchemy datastore's `get_user` function - Fix bug in PeeWee datastore's `remove_role_from_user` function - Fixed import error caused by new Flask-WTF release Version 1.6.8 ------------- Released August 1st 2013 - Fixed bug with case sensitivity of email address during login - Code cleanup regarding token_callback - Ignore validation errors in find_user function for MongoEngineUserDatastore Version 1.6.7 ------------- Released July 11th 2013 - Made password length form error message configurable - Fixed email confirmation bug that prevented logged in users from confirming their email Version 1.6.6 ------------- Released June 28th 2013 - Fixed dependency versions Version 1.6.5 ------------- Released June 20th 2013 - Fixed bug in `flask.ext.security.confirmable.generate_confirmation_link` Version 1.6.4 ------------- Released June 18th 2013 - Added `SECURITY_DEFAULT_REMEMBER_ME` configuration value to unify behavior between endpoints - Fixed Flask-Login dependency problem - Added optional `next` parameter to registration endpoint, similar to that of login Version 1.6.3 ------------- Released May 8th 2013 - Fixed bug in regards to imports with latest version of MongoEngine Version 1.6.2 ------------- Released April 4th 2013 - Fixed bug with http basic auth Version 1.6.1 ------------- Released April 3rd 2013 - Fixed bug with signals Version 1.6.0 ------------- Released March 13th 2013 - Added Flask-Pewee support - Password hashing is now more flexible and can be changed to a different type at will - Flask-Login messages are configurable - AJAX requests must now send a CSRF token for security reasons - Form messages are now configurable - Forms can now be extended with more fields - Added change password endpoint - Added the user to the request context when successfully authenticated via http basic and token auth - The Flask-Security blueprint subdomain is now configurable - Redirects to other domains are now not allowed during requests that may redirect - Template paths can be configured - The welcome/register email can now optionally be sent to the user - Passwords can now contain non-latin characters - Fixed a bug when confirming an account but the account has been deleted Version 1.5.4 ------------- Released January 6th 2013 - Fix bug in forms with `csrf_enabled` parameter not accounting attempts to login using JSON data Version 1.5.3 ------------- Released December 23rd 2012 - Change dependency requirement Version 1.5.2 ------------- Released December 11th 2012 - Fix a small bug in `flask_security.utils.login_user` method Version 1.5.1 ------------- Released November 26th 2012 - Fixed bug with `next` form variable - Added better documentation regarding Flask-Mail configuration - Added ability to configure email subjects Version 1.5.0 ------------- Released October 11th 2012 - Major release. Upgrading from previous versions will require a bit of work to accommodate API changes. See documentation for a list of new features and for help on how to upgrade. Version 1.2.3 ------------- Released June 12th 2012 - Fixed a bug in the RoleMixin eq/ne functions Version 1.2.2 ------------- Released April 27th 2012 - Fixed bug where `roles_required` and `roles_accepted` did not pass the next argument to the login view Version 1.2.1 ------------- Released March 28th 2012 - Added optional user model mixin parameter for datastores - Added CreateRoleCommand to available Flask-Script commands Version 1.2.0 ------------- Released March 12th 2012 - Added configuration option `SECURITY_FLASH_MESSAGES` which can be set to a boolean value to specify if Flask-Security should flash messages or not. Version 1.1.0 ------------- Initial release ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������flask-security-5.7.1/CONTRIBUTING.rst���������������������������������������������������������������0000664�0000000�0000000�00000012075�15110467414�0017142�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������.. _contributing: =========================== Contributing =========================== .. highlight:: console Contributions are welcome. If you would like add features or fix bugs, please review the information below. One source of history or ideas are the `bug reports`_. There you can find ideas for requested features, or the remains of rejected ideas. If you have a 'big idea' - please file an issue first so it can be discussed prior to you spending a lot of time developing. New features need to be generally useful - if your feature has limited applicability, consider making a small change that ENABLES your feature, rather than trying to get the entire feature into Flask-Security. .. _bug reports: https://github.com/pallets-eco/flask-security/issues Checklist --------- * All new code and bug fixes need unit tests * If you change/add to the external API be sure to update docs/openapi.yaml * Additions to configuration variables and/or messages must be documented * Make sure any new public API methods have good docstrings, are picked up by the api.rst document, and are exposed in __init__.py if appropriate. * Add appropriate info to CHANGES.rst Getting the code ---------------- The code is hosted on a GitHub repo at https://github.com/pallets-eco/flask-security. To get a working environment, follow these steps: #. (Optional, but recommended) Create a Python 3.12 (or greater) virtualenv to work in, and activate it. #. Fork the repo `Flask-Security <https://github.com/pallets-eco/flask-security>`_ (look for the "Fork" button). #. Clone your fork locally:: $ git clone https://github.com/<your-username>/flask-security #. Change directory to flask-security:: $ cd flask-security #. Install the requirements:: $ pip install -r requirements/dev.txt #. Install pre-commit hooks:: $ pre-commit install #. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature #. Develop the Feature/Bug Fix and edit #. Write Tests for your code in:: tests/ #. When done, verify unit tests, syntax etc. all pass:: $ pip install -r requirements/tests.txt $ pip install -e . $ sphinx-build docs docs/_build/html $ tox -e compile_catalog $ pytest tests $ pre-commit run --all-files #. Use tox:: $ tox # run everything CI does $ tox -e py311-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:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature #. Submit a pull request through the GitHub website. #. Be sure that the CI tests and coverage checks pass. Updating the Swagger API document ---------------------------------- When making changes to the external API, you need to update the openapi.yaml formal specification. To do this - install the swagger editor locally:: $ npm -g install swagger-editor-dist http-server Then in a browser navigate to:: file:///usr/local/lib/node_modules/swagger-editor-dist/index.html# Edit - it is a WYSIWYG editor and will show you errors. Once you save (as yaml) you need to look at what it will render as:: $ sphinx-build docs docs/_build/html $ http-server -p 8081 Then in your browser navigate to:: http://localhost:8081/docs/_build/html/index.html or http://localhost:8081/docs/_build/html/_static/openapi_view.html Please note that changing ``openapi.yaml`` won't re-trigger a docs build - so you might have to manually delete ``docs/_build``. Updating Translations --------------------- If you change any translatable strings (such as new messages, modified forms, etc.) you need to re-generate the translations:: $ tox -e extract_messages $ tox -e update_catalog $ tox -e compile_catalog Testing ------- Unit tests are critical since Flask-Security is a piece of middleware. They also help other contributors understand any subtleties in the code and edge conditions that need to be handled. Datastore +++++++++ By default the unit tests use an in-memory sqlite DB to test datastores (except for MongoDatastore which uses mongomock). While this is sufficient for most changes, changes to the datastore layer require testing against a real DB (the CI tests test against postgres). It is easy to run the unit tests against a real DB instance. First of course install and start the DB locally then:: # For postgres pytest --realdburl postgresql://<user>@localhost/ # For mysql pytest --realdburl "mysql+pymysql://root:<password>@localhost/" # For mongodb pytest --realmongodburl "localhost" Views +++++ Much of Flask-Security is concerned with form-based views. These can be difficult to test especially translations etc. In the tests directory is a stand-alone Flask application ``view_scaffold.py`` that can be run and you can point your browser to it and walk through the various views. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������flask-security-5.7.1/LICENSE.txt��������������������������������������������������������������������0000664�0000000�0000000�00000002137�15110467414�0016322�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������MIT License Copyright (C) 2012-2021 by Matthew Wright Copyright (C) 2019-2025 by Chris Wagner Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������flask-security-5.7.1/README.rst���������������������������������������������������������������������0000664�0000000�0000000�00000007634�15110467414�0016175�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������Flask-Security =================== .. image:: https://github.com/pallets-eco/flask-security/actions/workflows/tests.yml/badge.svg?branch=main&event=push :target: https://github.com/pallets-eco/flask-security .. image:: https://codecov.io/gh/pallets-eco/flask-security/graph/badge.svg?token=ZYS0AST5M3 :target: https://codecov.io/gh/pallets-eco/flask-security :alt: Coverage! .. image:: https://img.shields.io/github/tag/pallets-eco/flask-security.svg :target: https://github.com/pallets-eco/flask-security/releases .. image:: https://img.shields.io/pypi/dm/flask-security.svg :target: https://pypi.python.org/pypi/flask-security :alt: Downloads .. image:: https://img.shields.io/pypi/dm/flask-security-too.svg :target: https://pypi.python.org/pypi/flask-security-too :alt: Downloads .. image:: https://img.shields.io/github/license/pallets-eco/flask-security.svg :target: https://github.com/pallets-eco/flask-security/blob/main/LICENSE :alt: License .. image:: https://readthedocs.org/projects/flask-security/badge/?version=latest :target: https://flask-security.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/python/black .. 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 ------------------ As of 7/30/2024, the independent fork Flask-Security-Too replaced the archived Flask-Security repo (now called Flask-Security-3.0). This repo is published at PyPI at both Flask-Security and Flask-Security-Too. Please consider changing your requirements file to point to flask-security. Flask-Security-Too was a fork from the 3.0.0 version of the `Original <https://github.com/mattupstate/flask-security>`_ Pallets Community Ecosystem ---------------------------- This project is part of the Pallets Community Ecosystem. Pallets is the open source organization that maintains Flask; Pallets-Eco enables community maintenance of related projects. If you are interested in helping maintain this project, please reach out on `the Pallets Discord server <https://discord.gg/pallets>`. Goals ----- * Use `OWASP <https://github.com/OWASP/ASVS>`_ to guide best practice and default configurations. * Be more opinionated and 'batteries' included by reducing reliance on abandoned projects and bundling in support for common use cases. * Follow the `Pallets <https://github.com/pallets>`_ lead on supported versions, documentation standards and any other guidelines for extensions that they come up with. * Continue to add newer authentication/authorization standards: * 'Social Auth' integrated (using authlib) (5.1) * WebAuthn/Passkey support (5.0) * Two-Factor recovery codes (5.0) * First-class support for username as identity (4.1) * Support for freshness decorator to ensure sensitive operations have new authentication (4.0) * Support for email normalization and validation (4.0) * Unified signin (username, phone, passwordless) feature (3.4) * Two/Multi-Factor authentication supporting SMS, email, authenticator apps (4.0) Contributing ------------ Issues and pull requests are welcome. Other maintainers are also welcome. Please consult these `contributing`_ guidelines. .. _contributing: https://github.com/pallets-eco/flask-security/blob/main/CONTRIBUTING.rst Installing ---------- Install and update using `pip <https://pip.pypa.io/en/stable/quickstart/>`_: :: pip install -U Flask-Security Resources --------- - `Documentation <https://flask-security.readthedocs.io/>`_ - `Releases <https://pypi.org/project/Flask-Security/>`_ - `Issue Tracker <https://github.com/pallets-eco/flask-security/issues>`_ - `Code <https://github.com/pallets-eco/flask-security/>`_ ����������������������������������������������������������������������������������������������������flask-security-5.7.1/babel.ini����������������������������������������������������������������������0000664�0000000�0000000�00000000326�15110467414�0016243�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Extraction from Python source files [python: **.py] encoding = utf-8 # Extraction from Jinja2 templates [jinja2: **/templates/**.html] encoding = utf-8 extensions = [jinja2: **/templates/**.txt] extensions = ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������flask-security-5.7.1/codecov.yml��������������������������������������������������������������������0000664�0000000�0000000�00000000416�15110467414�0016642�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������coverage: status: project: default: # basic target: auto threshold: 0% base: auto # advanced settings if_ci_failed: error #success, failure, error, ignore informational: false only_pulls: false ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������flask-security-5.7.1/docs/��������������������������������������������������������������������������0000775�0000000�0000000�00000000000�15110467414�0015424�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������flask-security-5.7.1/docs/.gitignore����������������������������������������������������������������0000664�0000000�0000000�00000000007�15110467414�0017411�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������_build �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������flask-security-5.7.1/docs/Makefile������������������������������������������������������������������0000664�0000000�0000000�00000012734�15110467414�0017073�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make <target>' where <target> is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Security.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Security.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Security" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Security" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ������������������������������������flask-security-5.7.1/docs/_static/������������������������������������������������������������������0000775�0000000�0000000�00000000000�15110467414�0017052�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������flask-security-5.7.1/docs/_static/logo-owl-105.png��������������������������������������������������0000664�0000000�0000000�00000047657�15110467414�0021645�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR���i������uS�� �IDATx UYp񆢀W]üyHE -RIMP) Nh8[hay-Vj,x-ED԰|7g}>}c;{y|榭[n .6m4??N׻֭[K/tڼy^7׼. Z;j\JWoW%\2}ӟ^:w|;ߙNַ^`-Mo:n~s{NW|3e{x~3#9眡??<}k_ wmM4ӝtӵ}:s b6ug _tE7W0wWUvP80 Gw}:\<kRPK_u\!x騣?xShxKWDp_Z}LKMrP:h�'+xK/tkHa83 :ӧWӏOeѥ~"O|鳟tO|#/~o,qMμuǫ_ʼnw0)V3kߗ~v7OϲJ73ɟt){#u3uMKؼt=Yvf7Ї>tQ]wuidG㽵.ϱ"}w!y< o``b?ϐ6 ﬧ3y<} _̠ p=ơ׺ֵ.CEE^jWPU!N1FG_6tk^GG#w /L(=S0~f5 ͷh?72i[lM7эpMnri[az|EјΩmo{Ԟ{9bz!q GEV//CIezIȈ4ou[M?C?4 W煂_}ip u TBZ+|CZ郟~{3pc=37g/r:c}\;*v mi떷XgvZ$Fnq[ '9-t_`vѷpwyGw >tZe> BN;m~eu^/}i G<!H, ˿5qw.!f~.w3g~gƻwܖI m n0̓F@{gygV5^|;D __M1=ϟ>яN/x >_ q!!¨y!p79]z�i׿0CLKKA=$k̴ʯzdz_]`({Խk02W?�׿A fM,O!_WSw]q�LSe'>qenI.ch_4B;8}c:M0'$+c0`3`F' Ea#Pz=aJQߘDf!3঑hG0�l$\h\ 8  L@8AIƐfc$E$ozիF'bXVS_x;4ka,<{p i$g>󙣭o}! L)6t"F`o La64?/p !|S..mZIiӻa:h@JC)H$U=ҍ(&k$][,68$c"M\O/N%la0{)/#ʤ%4$P ADt6_yBD]`\O|xn#'eo!(/yg}:C}'$υp<IC|m 3L><!'G=j46&3a8묳wBcӦшmG`nwq%(Rp`DT&ITQg=a 8`3ƍdH'U̼5"eL <}l8_t!9}sאpHb31NDƐh,xK}G(};` a1U(87X8fy&KM[.)EK < F[lٲz:P0$&L% t6s$9H0H>|x6G�~k_;Qkf",ff0{|&9#8# "Vf5Mm C0]!*G?z Cidѵcق!@PE;`yB`!d!,m2qG<bH2<`H & KRTJAgҴ Z.hH=&1C#0E>h@Hi &!HD�gU7C&ӵ׋Ifjh{#�@@# D1 4 `Aɟl;i\q֗Ei4l&zPsI_jڡ~A_G%x%A@}Ң__\wrk% l * xQ̂8'9D"5l;qfx b#6))!8�CJ\8S-fų$큿3BvwG3 "Wh9Ap-/~@VMozӀwgG HiL O1Ii/W'fbiE(dB2L hc"Ch_BA%$i"՞>f SOf4VDoza  )6Ly4(vK"2ip ?` [IIAE˄ )d=.mf#H&!#CaZ9l "    Äinp/gn�ԏߘ6D�􁸞Gl00F`!dM[}ce7{}G;޷wgusHB )�@#:˜)0dR901i@љOΘ$"}0#:dZBI:b3H~)g&1y41xoB~@XLD0yYwG2p g`<28d1EÌdb01$H/~GhR 6FB:)f19GDO̔f)3g^H>)#8Fi pQ|͒dZ$hb/RLy0&A|�B"!�1R!Ary ,bΒ2$i6Lxh$?: ' gPda! L�)_,UP,A#Lk>ɬm\kcsy&M>!$ b$t2 \ `N8aS8I@hD1UK1i`@ ̩1,Db6I >|o^0<KX!Œɿ+bEֻZ"68 Iq&F[5ͦo 1/|)B.sf4Oio3Mf~~jM$YL bzi"cT&А"ZT '`-yC;ܭuթN!A"d�G{HX iҊ|Ba>sL>|sq~ K1b#8B 3̧@ڃN0:ąw[Gj-&`o݊\(fkr1�Ci% Usdgf\&֐@mnK4Ѿ(Ϝ|+!2">4^A@0~3@aI]gy-h—RpDP(!fkÚTgafH?I9kq `8|.0;C@> 4 0Ϣ];m5Pm$܌A- "HxM{~'pgs`nZ™B(aI{NjfD4KBp5�4RAIT(Y2[K{0%vi)׮O3" n[_L0xhfMǍ‡ɧņ, ڙk؈ڐln#2fR ,q c[ c 08}ߙ3L<L'Zb&)%̘ dxDo(Y.$w8! \pp#D 0a`6h~ {6 ȑ8Oy7~7#@Dg|o_44�}#w9R̴$o%b,ߟٟ 4~熄@l>&^k=GE_1MK1qh( 8G+amv&8 $Q`!˹99,�g,XDv<ZyL9Rv iB#* L+^Xx"us"0 ~4�j6_{3]*"h s22sxi[a $4%̋Γ<?|`D\pi$tܧ|"31Y8ͲcDA\>Ԧ$�avua`+`ew3տmfsnǚtBI H?D1)@M _ihE|g-Dg3ge!'e!.m}۾ܚ(34E;f#O~! $ BL/!诠nG.#ί.ͤ^ 'toEw� ,$O=UΗfH44�9H!6 xKCtoY7%,|и2+} [-My5#D 1 ahT~Ȏbm /&1q 8jml=hoDsiEJ1i DpoD04#4[߈뻠gY YNITAAJ.'Da|FB"ܥg8dA4g0'*nF{MhԒ2#2݃pLiP`D..) =}]0#\D ?A]D|π6wIS6^ZIiLâ�|ƲcSK:RH -n>m#MCo %DXK-V1ͱaMLTLc(3P`̩}@h h |Z>X?F2lҘ9i"|jc۠vo-JR�! [>˦]i{ P3I"x(Y!B(, aZdjl(Nh�RI<a8Qv-]}2 vKcxƘ/"Yd vRav�0I&E̜ y=Vfb� }Ćwc$&Q{&xyє1g_fQC K@J2fBib[~0恟F&KwӼNhPk35/ᜯ r5YIB[ ra.K{9}-`~ OvD f6Q-_x ̢ɂks&1Lse چal�< cXaRVev-eM8M$"WٖxvLX~w6M|?P# B8x9KKC r1f0}טؾP< At@1v' >V3_3_ݙkLx9.'ڹHf]-fKФKU"#]Z],@�Rr!i"`85x≣>pLp?O^ 0YVt ,z< h7?V:)2>ĴIҚ@!bl@+?G ryoN{{L䅯"&H7vy`A\1$Xl RO0h%s_ TLnice%a< nl$$> &pE||FN!47V3i�O�_Z/”(I#"bߚ"`~aD<JCD;"b&S6#""`4N>Lge*i-&X`Tѫ}sx"XZjp"5 Y+kΟ)pΚF٦%\2œr0 AI$4a TcZmk%8)0IƔvJǘfK|Tej Okh zLzgT+$�On>ְ) 1"z/<""ⵀfF`BK$Txl6k`" GP0ՆM;i9#8B"IC}osNrV.)Q>:F 9I䠙0�A Ɇ|A p"* D& I0!޳T*B8M3[m7-Ч6I;t!(";07LE<qgD{イmO51ӂ~C&U !'A$tD ] }Mlqk VD=im}0HRXZ'b2=4EvSH`F 헬B[0CH;sn@G+<A%0I8($hUk=crI|Н3gۦbc7!2^uc&\ Z7z V iH7e|"L}ۢeu5 8v* Z@ @=GgJ61FaI#XBDNcUh|b~)>j+a (|U;pRZ~a,Cѣ^[]Y]}m>c˙A 6gV�"le̗1DҎ)#$_AskSpLKH< ht#P'6Jop�#Yj|%4䠕4^Sdv]b L8; LMzޘ"-КTF H>MrhL$̏.s䂋YJ[Jh_aBݣ!4zehc@s3AKnW#& ?iӊ)&-A㤔f9q_œHH#Lm(tE, X&0 .&�Aͺ3@4,ĔBC<i'M,ni ;qC(33nˌN,K\!L4+KV~6&&$ 6 !bjH]j%=y][yH,=yCHRJ,fV?-Jv, O}S@V{7fAE0*K1ir.$i<o4de HRt=`*%g[0"Rӟ'x`Nz!0Sbh0ai T飝wW~3y%aZ?4OYvĸiJ`HV/9c,0'l�I"Jt&|YLV94e&W 3a|u%d31Pmܖ%Iߴل0S=0կhXS]k-:0wk\Mn<T֥X!bai7h'~R$@0<TʿWa:0f`2Ӕ�5�7ڀxg ipA�� �IDAT}p0g_K;ijّZIfVpa:@#E'dJ5[QQ@1_U $b[4GRLS^g?` ZK`hg|/'AY E X79{K�;[a䓒7YU^7z0IE5_N =K0 `4DL:kE INv#FErҘ(̀L{)l;'f5>n@ (_gu1U[ɜhP^eOR:f2ݯ >tA E ; b<7i꾉HApvӘJw&S6 KOᴅ,.3_VUdbT P'SOhC -uLil%"3P7dp=Cn&(m(Dܦ]eW b!/ꪢFb3X4�4hfQjb if1NBeũM<sbfO*@RkKݹ�r"Q $ =UEd| <MaJf &?C3I?&cy &m9VP,L{i}IMd"8A_JSo [+[t-H'giRpj1XVF( !jMf[8{<k ȉIZIB9d#㯴gHo;h.`BKZM kHBPۑ'A! ^T�䎮Vf\)Nk^̢[Kr줃Ct!4I.i�~ZH.bD<Cڟ򔧌wY:o �zY4 s03L"-Rdi5U\iSMgZm4R]t}sQ۩3RoAm& f[f Bt)�oG[ >(@YvPg:@e_ü4Sylh29as\1'm.M a@lOxI[u^|k)sWSBhQ:mq!{$=D pH[4hPkhb h)3[UH [أm~kOR ވ-6R h0CK赮ё2Yvt,@ K)O`C;�41bF*݊Kho ʹY�ZdXYfjEE+V)sxpL[#ߙ4ʮ\Jug2YLrHe%=9p҇Jj1 M:>jB x]f 6ઔ@4!cb?F ү0\#ӕ3f6{NXa;ي.whu)hI6SGR&JC bya x)V!�Lf)گV~iZɊ\P?04h4`o)ӶsuTJ-W9# w1`>-H_ZSZCZMY;e#`.HY{/HhU;vUG*YLE!B YȖHa3\ ҠAs`16y~v{ ˜4MY�ʒ'KIN!X:AOV흘Ly)0_{s6rͮƁ4L[L-ڪd 1d1f@Z&�m[*f"74Z&XȤM>b*+nw\Acj\�%8g6YA (hh6 �`i! hNa|[5;F'=7͊&dp,B ^A __fU߂x.U֠<z0sK.`䉘lrD`:4^*r:"C D[ff#UP\maE^W"-amڃ($^I˳͂SiM'(^p`HLha3^צ5m*$ QBϲvL*E9-) P=C*t{{c$R*4'0jg:ܕ5 AXn#u!"gaCU<#n ֬>0 }PX|&36i}Eቱ םfRfuB-&dK+HoVړ/6 i4'^!@s2[^kDfedFi`ggv#bg; Dx `ӄc=I,B h.mAF їI\MUsAd !W>RJsFGUWeB>9F;\@JJ`vh^LGr!꿰&~ W?m販<Oki_]7ꯜe5jQ*ɓnky[G@!TBOB"%)vAs!h">Zml)Xkڧ׽u+e 1lMa2aĀ Bb]m߾+34\a lg#ɒ 5 "k%UF�8"Uw~bъ8Uo k4(r4K >xL]8͜pg~(& ,SN U5cWlkw=f*lRgLhp b9gR�BYS)c>\gH`8im 2Jxʲ}%tpymN$ ~)qԑ�s-Fa8ʂ0\�OL7<* ZFf*Zhvfu;&5 lC:04SeH�B3GXD`;8~#vmQE380v݀VjrMɭ),49m}#%@ju,4<-}AЎ6yZUxpGBsG;H u hG%$�ؙ $ WV!+Μh/2G:M$�|U4;&V։a+,ttVRFc؂ǿ=Y KFl.Ȯ g Q2}uvMD`'kSbwm)!zL{T+G| !" )$h!XwsE kzgk7SKk<k`,C$2=ֆQմvLjrΡh| 0oڿo"!wwc KDmn$;|>_ͦy+R&-Dʴyp__aV`Ɨ"»38Hv~8'$1X-,@T7;Pl=6(CImWUR]8ˆ*T 蘉i eКhkw2`kiY1cA/Dl+cչZ2 .s]&pșVOjTf1K:$-ޑJ7l [)L{$7Ү@L_[c*闞#I/턐UJ`njV3NaҞUɜڔ\,2_p=VfXq!<h,, 3QD5MVT]!CmmBYb~G$RҫU tg#uN_u0YkJT\#ESNӫ'F4wԹY~2bʬ%>L`ZBS\�η$x ;I1 H=!<B#83(f!)٢I1//zы! S&1#[e2epUY h*,ϟ M!ڇ.AA3 LKnUa4hIUBp*MJ(f%-C QO;'�ӼWVZWB[" jgHy;-EЎ Y iֻYf& ae :UhQ|4F*ծrD6/+`u!4˜ 0# 5SL*4| _Re4"H0%);j[=yS,a�^)mF '" õEzI.Dorh4v%ʎi(Obj ]iiE|GBKųp+ +@N1  ^o@)(8bB~oe?_pᶚԑ> n�涥~Zߕ}6P DUwm!qo-3dy>:䃛zFѰ~8вQ4HY) 5MywjL3HQ}d�+BH?ݸŇ T4# u &l.^s=Vy$7߇af4C]ΐmДIEx7Y2?c̘Ӝ$+!W-{]F 6Fr6Y0ϑ,A�5N$Y#qX 6<褼RaiMfּT15n Z" <; L^AY.Ï7Q̧v{ާerQEeTX4ʪt =F]ǜ& L MkU$qĈH[D􁘴�)AU-a3%]4`j%lOJfxDvU(рU犃`Ў sv Ao[5wtwEhG� 1FH<_Οhʽ#4p}cRy"yB B,7f2mEVlK "‹od>9W83Gi8/B„BxJZ_,VpjڙMSde& !Evb])\n)}h:VK֒ }Gr֗iBE;Rrmpg˟ɴAM0jGH0xtЇnhS'eU0rTq& +/zq[v5PnZ5 ,!p䨅%-�c07@|K^ܖC _ w/= qN&F髂dgÉ(:6}l1�YУ 3b#�xpST9H~9'R^~9vi֬a+o!jYKJeCK,_ a:+Nn?DkM}OsD~}T-{w&$ҭ &2feao3$W"-*jG FZ8L"$SI;6MM3BNQLM idz #\tEFamvwU̼D.Xܩeǂ]3.-p0;*\ga0n!H&"F:P1XBLJ҉1Nlʟ|//ˆIK[8Uq'J>PVc'}#zw48!KK.M3ѐP3.Znb`sE|VFPŝbw964309k)IKpo*0"̀ ]uclfɴA5YL<?i>'gV}  ߩْDG�F``,>CA1  �f k|DDwG ŒuT-B#L<+2U73Of&Z\qG`P̈/YpCs~Z1mVn$2L:;̖WQiP[B$$X!Ɉymrvf@#{~<mE7s|Wy^_S2 MOO|C [g1Ek L/>3Zs-x3`ƘTa-6><6̤jb]]8f;st5 8Y&A0�18 #|qWW6>GJI.MiKsI+2`3f; 1VS<$88C Y MJȝ7X`@;:l7:<-L*5 ~1 ~2ڑ,>ed@]~`! q !&˜h&So=D3夝N0Y<hJ g>%<1Ҟgp?O4O%HƼûw#ڷa;d4ۨ[U'�rQG珗&$BX� 1ׇ[1bHWj m$}r ISB` )E{\s8)Bp&d f&@2$ĬrML#m ָT9w0)�h�K:HV5R1;D23/;䓇+͘">a錉2h[@֮JdhMߩaüv) >3 hЀ1jA+hZ(pq /KT2d+SЮ:Ȕ'K9ӦB#&UL6v>C0N@AMzhHeXgLECFQs+_,8;S LE飉b[u&m<*nZI^nSXVWĄsHe 62K9bc4mWg1L8Ṥ>39=GӠl҉4-[kN@>3"𳛑YC pmJaJU@&FS,y/s$UT=]A=R cn4[O#fU�?`a<dCej̋un~14J_ޭhaI3GVR=GMA�(%mcl y9w�ËVb DZ޺~L55rOIg3Clb?BB1S*V$pmL{UhRXBKCf;ݬdKfњ.Q#Ķ#$`ah/#0X=ء99~So.1jF-s- DuSW�s<CK4'_8wΑ{iSs;vNbXX�бowE+JD~m᯴1Z+` Oh9338`kVKhM;iwxLg2ڙrj1J(1Hft 8J@sEU 1i"L{5Zari|A(lv{yx8ü24 aiN4b6 |+8ni8̰M%[T;riR+iU)X)N3"D,)Sb{DdHT# GL| 4VL:kxr0Ij[MiYyu8HTĄB"~4l+<ˌQϚX?| Iۥ�;dR-f ͔M kYS 4�?I2JJ3A#r�Yߙ.2vT"}{a4wKc1=+ o>F$su"mY4}] BiiK`ht}Y IA0HVS/seHs|bSYN&w1I1=)qi1m#>;bUhy#K&tvti)@U9 %A�gM@cTƀ&~'}@a< {Qt9&R3PDx%I`j!&bFmW9ܠ0(Z&>�Wa&0T|du4-?t]psTl=Vf,�Sl40{8QRaH-ƔB|m1CPeXbH,B{oL]QZW=_'K{h*qe2:_IT3s9AlBRpfv꾑D:餓ȻQDHN'5H#$VTD4. >N ߙC=a,?n6$̨j tr~7af޳F)p1zi?&5Ԇ9aʷ[֋]z.lZ:T��^IDATiàbLga96]iJ4gGaFId(}&Fز]Mt0ο�Wb1!D"g,I>̿WfKOk}jk%NiBTTsdrDbk>ۘ3y .06xfF}g3%Dx0  Z`m}"KiP >@Lx,4tK+h訽nE]h yMR"Yߘ@vJBDT-ftރpL ) hae0$Byd!9c^IY2WD녏U>36L%.}3sU[<;;DTH5 Ddt.ۍM U#R[H‚"00De,YExp ?pfR c}fO3n@ϛE;`6:lǤ t46c+,[t)$TH%Bè ݲfDiBe;a Fģq`6 HE*4gJ1 ?8!(\ny~lɴ-da#ۮ-64P'퓦U[#ITyh_;9}(0HGjue0`1FtPbu N*J3)" ˃HcF/Ǡ$5&wrGCLk*ۖ66Z/PרrϫZr駟fDb;ON@3e>/5s Gh)"BՄIÔr%t.{#0%�ekNSO]9 c!àڛiU9η =EտT^O9-FEg5a3,+F37-5T`y-IL eAڔ{%4닶*ihB1#Dt &%xM*tr1I?J+mS@vi{%O i6GIk}#MhiQgUGh>I;X3\ >_juvg>wL# bن(p4YK絙�o#J0:G6DHuAݶ((ϻdle.%\Ɣc-t衇n5frzHe!$n$}ħ53]x@ii*W#rkҦD6.ah !c^u_h_zt`eЉp1p@|LhUY%﨟JI` >Lٷ,K Xtol#V٪Ɉ$Cf�Rmch@H87YEh8jI+ I1e;0fSc8h!7 ̹Ɗm%``p!hXQ谬,>mѧ5MaC6ŏtB")�)NCȊyAa63Lf S0i/D1t#j[:^LhK*~6 ` &#b 0Pj0a 1c]Y {U 1""1AJt"Յ~`CG ͤ"E=G vdf1wypgi:@;0AH8":@PxaWG]Tmꦹ: ~&M[b Ry{n!Y�&J$F;MY'FHhЀU'VʟByn #3t[i2ݩhj!"" 0<߈ha@4  ɑO0!R { BXg&&M`-C˜mv L,`ßp<#"BX/!* 5ZNZJг]u+c!h|"*k?bZxLJ:OI[!D}̄3l! !%pXK$hw;& ѱ 3ZpQ*L"p2뿲SФA\UH1;\i� W.L"R{0[>'ɶT]fP0iMDi]WiG ؘ1@hR(:/r5hԞy�s-C Ѥ-fN3,!-VG)~Ψ,x! /hX`; LhS;My rD?͎9<@kH-MĨT4,-f>a  XLgh~wB}l8u ^/QwSm~ы^;5H3�40#4h[~v'zיI�+! fNsy4ӌ\g; @4LJ|j? 0!YTDG >8IhЪ7]Aw= 0|%?[مڃo! $JBvnC9�pU(C&L&;)#!&{:.uzw !̝ݻi<xaY 0cpbߘ58ыi/mxa8z9`}(vm,U� 4d虁jvL9|~�Uɇ3�#]W=f �ȱc 0ï`mLfr (Sێv:# d ,xGh1.˨J2inQ#7uLh ?̬̻$7|[*q, 9*`!bS4 ś 5j%`.j_jC2dp}1/LA87$D`fCMs p\0cͦ 8dVlT'|7GIIi0XDa4M&B;ZX 6x[n"6M1,y2sF%"<c* }U�ׇIӦ,uRE >!XS,uyD+̄SW:ш# ¿ ڤ8mWnsh t8ngFB$$ \kZ%b~~4Wh A t ؠ �/twՇ0S:"CŽ;&Hq[h:>vxۭz೭8 MH $;b=R.h%Qd̄aTGYL$,IY'汍VQTii3)VimO%˃q# )\B}W]^4Ҵ3%0{$c2FP2SHCTGLFyNKjh!BeivM*Hrp cZ VQ)<N;m+U8E* t TU:̰*dA|60iMj.t t\U!3iOX+=j{O^pe ]HL9�p#:&o~*SVٷMAգdy~i,oBG#il}w:j`p1o`֢#0~A)-U EݯG0-ıO) W gieT;/ G%G 6K!6=ՒkE eZWSG%Rng$f3?|2rv"7JvV_ ,-ǠE},xL<FѤJ|YZ)4Kk8Ĩ%\ZY]Z^W2@B :|s=h@>�B����IENDB`���������������������������������������������������������������������������������flask-security-5.7.1/docs/_static/logo-owl-68.png���������������������������������������������������0000664�0000000�0000000�00000022345�15110467414�0021560�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR���D���d���?�� �IDATxgmUu UPTR{+jKbCDMl1j ^AT`CPE/7o=gss*sg9c /ܶnM/=sMn2?vii۶maÆA?C}g˦]veڼ뮻N_.h?m޼y:tW 7mڴ |ߟ}kO׺ֵ+r .>я@_‹Xֻ #w_;<]|b ƽ{˧O~iƍKנWY|ez8.ڈ.st;aFǢ_tm#_g׻_%/yɴ~Mtsw}CFW^y6׿5N;mo{tao[nAg uyu^km ]g>vV^SON<?{f5qi= =�?#hpeG8餓F[}ibUm%r0>Ore|X`[LJEn}[O~;Y?i:ǽJ~g>^t[rԧ>5 O! ÅY:0.7O~r|~4/`u{@-fv\:י߬MGoi9ίykơI>w=e(#o~< i@}݇EQ]>7]z? =zSNN Ϟ}0x1�CBΚ^W gyk{0_tvt?a$ŌXzֳ&I0�Yl"ȝ|%*խF#}K_wO=\P/qh7~Oӗ`g>HW >׾v j�18IG9C=tC9d:sF޻ np$}hoP2Nb"*Y�B.�W=7 1\]~6|chdK9́8苎w=,G= Lcx)O1>M1?\�\�?hL:l� ؊0tZga֭θCBY:�2&4a(#HYsαA( nܴi}ݶC0mh%# ~E0f~0 6}s2lzի^Ec&$^(c7t`4 SoS~($j櫂;OA,9o_t#fEFtp縖Ї>tȼ p7,R)Oy@[twyַ5|HGGd o>SEA7a~_ �apס{հ \ӆŵ҃�ie=bю3W $$A()vfFv UV�J,lùXƵ}lGwj +\׆~H[8Cq]y#tҺec>X?|tȂMBP 7p7?! % .J>0R80#F8cg5p7r6q~G`dΏXX&Xrׁ<PI3'?>Ϗx|[^7,^@w< � ~8^%v&fNw*2I4� +o#t JRu#F:nâL\> nY%-'b=g~.i}!c~,aj�kCa Wz0b3,$pO|G=8 =F.#{S3s! Fi1kpYp_W7^ ,~:0p\ +W %[k~w(1w%JIva802|;G>1cj.ψ;\ mjLzZː6'脂,̯]z!9 PG LI`P9yck&7S6:O#;~k_ˑYx@L,a@c)ڣkFsAr$o{�b2^H_ @;nQCJR-cȺP\hB!5RDT6,UJ +c)2rp5J{ֽX կ~uĪOYqa</sM <Gv/} C%{t2:@1Bgi)!t`,hud}_/I Y(X"`  H$sӱ;[Y}Ų-. E&AJRkbxS� p@;Ρ9v${) z_>X=#Ȋ)U3<0F?1g:c R\PC^nd� nуGyFY aeRkuQ#&=$cJ+(ʥu0$ ]k�!@yc4R,-je@L(RX0XCy�`o֎~xmN26V�:zp S[ȴkOjXkȥH+jM??GYm*�B,sw?"%h�Q�o( ;oիcҹT � MR [\ gGXW5l;!»_[0`K\p和drg< KR,Qc`t]XqM5/{F<KLrh܋/~GЇF@w y(6EF܊;c®QGX},e# -tMTۤ gYgMќK2n@/x _v[|[ĐvM%ekR&&&USs.`6MYi -)pD  ^S^7&Qb]Ė0-oG̶!aǴ:ڤ׹⃿YiZ ʢ:@ gm\۴ p).V G!6{Ѓ4'E9&HRPPQ m1G V)B:6bj孤D2br݌H Fd�{�ʐ gtBH9XC(6w�Sb]`Qu5 љ�Ş'~@ʙB7q{�evHi<�9CcZC<Ģ:с�mRy�a ڳ m[QR!4˹W$kGRf6j [!fp9aX\ 。Jޱie7Mł#Jf�;*lӇdE2=qyP\o}: 9yZ=sbzǚZ� yPJ@QsZJ+.X%vwzԣD)av7 nj?|Ӟ6D:0䳟1tkAACQ`hj:η e8vT=Zdv/WY{LwYb[p:6HhLw @1ݬNK! v-x$3c A(ien4BMмY!oѮ3"Q0e=ĐJJ}.QkYv k팕U$U ֕7aLcՋH ɪw5 x촲cw0L;`L`BW8VsG`b2$Y(3_~5?^ސfEEAjEeecX{ a*v(Ip.))YL?9Xd!`qrs%y&�W]z?91 zRXd9KZ7?i0Y[ 4Х�K�51 3: rͬŶ Y,t āEt.�m?& f)r(%fױa ',�Z68\X;Kw�w~lse*Fu n䇕9Php)9.SXqPS x3XbԢsk!P�tKi6)#Tm؆y>0@,랯]V=iet zVB&8D$ ū;~D,>cay= ` 7p;_ӂccҖ_7AqIJ Pb%\B!"1=(Q¸̸ƺ<�=Y#Q y X&Tb,;0@`CXPHhL,H 4C- �B|Rk,<10d Msn%ΑEn2_ blaT8XQoǞJX2*$(�&r_oˁ9 &>u)tNi۴R `1ReՐ z |䕮k B6b1 ]=ډ߬<SXP0ڴ.*8Aa:=x;f)ˎCt-t$8!`}gmF1 9x5k5Fܬk#Iô7aܕR 8ĿzKUš2$f%>`ЖQoǽSd,4'0y-5=h>cgAE+v{8=#3@eqdƐ( f] р[X>vX0:X\ 2߮{;R)W)<c,KXXb8? 0J n'-XS.B@ mT`u`vðq.ܶfrmvmdL7C+kb0mZ )] mڗ,[ .6PĪ-lZyCߔAm` 팡0o4һ^*'qL"T l憻Ǵ+<lmLofX@-XK+S|0 <k_;(%Za* YM6`b0ߦj3O[}kۢ9i;V@ QT4r&zU1dE]rEhr]Ji�oJnA= Lbg+tTí2{0]=6|B@ WuZX E], leM{k~ {͎+_Ju}B7Ov *(922d AiooׇxUKꌵQNXC $imq8KP*�H9m7-r"1Bvz-YkKo.)yX{1s)�mD"P+ K*o�( EW]#bk+']C\5GL耪UᯬG)y}axVƦO(Q @ 7{9hdѣ<9,JEγ30i+%#&�5O뜵Tbq)7{�k,[u4I़AчdR:pgːhVl^CVF[2EZQ F h- (P3➀+p_2=Z*%=K9ʜtI/JkV뜫BȈ hbS Tmۢc2 hvrKJC8(nP%;>9h}3!2՗]:o3zhVӗ41͝ά*2ꊢwRYu2@qdzMC!{j�`b zb /UH%Ӊ'86ԏ@>,.&Fg[@50"1;&wPE=It5Lh&,HPXr0O맗6 / 4:*@5脹ȹ蘹 !Y%:2Phd5E?H.9":\y�⳶ ^\zp,xÿ}I]39I�&XekF J `Aj\a hXsj5K^ORbhhG6W}Lv/r?e{^'2]2[lzˠ4diQ/C&m\]uj(,L;(|wi�˘[9ږ$'`(@˺ѹiB7,b QyF2-6e,7VH�A;>`(_1ɤ@Zsf%O- d ` pYq7UJ%@lj1bHm͂ejKpXA� T@l-뙯4ƴ`D6w]9 c@ΊF 1E[&\X_UQYy֢cTъJLj vxPuN\xo= ۴#NQkԃyEcJYev0Ú~5Us` phA^Ed(%(qG4Rj\Q 9<}U}*&<a,T`<|k+ˎ1fe;me( LA5!{P�e�Q ۂk\ 3z}Xba!F0٘g*%+f~FTb p%ò#E(^55}o;wܣڝV.x{;\<V7�c$+u#0ldh[ms!&{F#Jh ;Mwi(EAn[ln`;K榕^X8ۆhe;e1E2LD.!b d#Z{&j7�m ~S@uj[U9{;ǰn̰qޱf|M[}Üߧ3<#,1,f,p)Yu1X -X^P|جdF{Ѐr?Dzc׮*!¥uՏh;_GN;T�pWҤ󔷗�pXC8}:̳fEř!!,nj, 0\K|[ Җ 2?p"NXZIsoDZLk@  6rվ`€׬`A4.�EI 9*ueHI ){+(O r`X-@0Uu F4/V`VAX+`5;~S@Yt/5,;6?Մ>2f6+&4u}uRM`EZn{uTk\7Zu0V[�OR\46e}oE/1B_]eսECxl؀�1$+A; a΅Oq8LƸ^umn߅@js!ka/mGo=c (ksY\4T| jQ #g->O+;te-Xޤ싚2SJJ%(K ĉkMNF!oE1}kZckl^=9̼_Eʦm A}督XkiQ߈R]qfՓw6ʋb+_k%r+/ՋA`^0^68쳷%Ck@2)Aؾd5|ɖcRf2g8@9oҗ&DkgukfL6'mv-mfJԷdq +1 =Q}>㾧>G[ez[Kۄ XF2)Kz#e��IDATKXYX�ɭ<2TY)y8'6ً\~GQ#5Zu{ݣm sjCV+g߬PxSХ)�>ZȽ=+`vG +'R s6h8 ((:�+t< }ݮgb`0rܭO_7& 򥾶IJ>e< n5XOy,:ꨱH8T V*mh "@n2D?IXa-oH�@e݇)^V W맍5WهZmh}{ 0oiúObՆo?e4%#_pNH P ؊PڂyKglMxi?,4,G h؇7"Q_M8-%[hJH` p[s@ԓ`,.eP>jsI93Y9}4BmyUprUqx9^=÷ K_حT64jBg+?0ڰ2Vh.ȱp_:($J%#C׮$})P\RޚG+j\�12}VmU`<kmʍЊ"`.Ip JLLoH^\ }WTbWD0hMܴ3B14pj]ӏ[c&C<-:e gAofKpBzS]c[ E{W�jݗ뭂HjYAy%Y εlM�G|j ^nc] * bKUPodm)>1(}9LykF?\ 0F0rK_B_է\ی\:Y窌т{W儱n+ZS(훢h]^UG˾U9& l2urk|`m[nlL…Pd4!{'1 F֏ݖHqmA?<jWc]6hoC0J-h]ebu1uX _@w̿Wa#ւmӆ(m/|JVߤ2GumWkf5,N4u:G9*}#jU_ܲπβ5SbJXX�Ҙ\Ta����IENDB`�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������flask-security-5.7.1/docs/_static/logo-owl-full-240.png���������������������������������������������0000664�0000000�0000000�00000205337�15110467414�0022574�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR��������D �� �IDATxuy -m V {Fؐa=7T"Պ[Eqv_\ds}9y}̛7s;wnl\s͆w];4ݬJ菚y5+J9^gw3{K^ﻕϲjզi>zĥRT*Jbv2/{Vַio~ϚZͫ_gmV[mfW_OJ* ^y>裏6?-rdo_y+ZT*JRT*W+u[?A,ꫯ;#vkם NX*JަSO='O<DqGl_T*JRT* k˿G_4?x|yg⽯~o?Tzq*G�/|ljfUV|+gQkFmjJRT*J59?!#GlfM7mdhk!ƿ/pW+b}o4z׻Sj7ԯ~6*JRT*J`0 /DHg>g';/~1dE}0ZuU#8!4wC4�Ϣ^6/?o2eJ}. lp_7M%JRT*J,Xw|l5o~'\zO~�bz^dA[\~& 뿚^;6V䕿}kp}4ǏoG~//wK_،0RT*JRԦx+??i; _B|=$]Z: o.0zN^+$o|csI'5_}sE5~x䑟qoq}/ 68JRT*J@(B e 7lUzUs8_כO|q]ii0a3;$Y^w}QTOUcf֬YW\檉U�~`:.JRT*Jmi~??o|Rt]w57{y\JU !0iҤpt'L<C?~#@nG= ZsT*JRT*� dVHgůƎ0]0dUJng˾::k 6h-\\Ӟ~xvi }QG5[󶷽-~~T�2]*JRT*Rܹs_hf:ngmfN(v nԩU R'��s(eKO|y^@+9믿~ 1�zr8N&g*B _?ӳBJRT*JD\hZIvmHNEQF7sO~64fmk0B,\'lx(Rg ϫt8>K13?|8۠XJϙ͎;\yRT*JRiyhPne pQ~o^wqF o_5U4. ܲEҪԿc86mqiYGX`"ӦMk?fu׍fUĿ/ ~v_XBRT*JR44xY`$owk8"ol旿ep~8t+ @H?i(9?K /,m-NqؗŵߝOyV!^ ϙ3' =3pr0 6Ql wI*JRT*Jeʋ aNvmnvQ!D?SsyȪDq j:n _2 ~ V V 5{.@xsh+lc.ekʗt_~jE3ꯚUWi kqG[uC(uш#3f�!ڳyՠgxZ9JRT*J@jnPZt+_�:aa]y[N`,_X4,6 ɀ˙ U )-n.M/A۩ڌ7.-A :{ E)y+_'ٽKeq C֧?H~{6݀NsAѿy[Smt JRT*JҊJou r3[X կ~uT"OXFqUV#mleZ߾;ޱx \kssO~xtB ۼ$H~)Sl{́ŹҢ8WU3>wޙ\_/Zl߸Ayf/>={v˃a٧ؗFkT*JRT*-tO~a��?q8|_-"rC/첀/ᵋQ;ݙ3g6뭷^馛F~^3<Z:s_/D /lvy.]t>^r ;Cow8q󒔮qs#ؚP)5~&rnsl*0>*d_oz꩸7=zt\*JRT*JA _q!'$%Ÿuv0@/QI|\mVop ę[`LujyL0 ~}�Os(S‡A;GSAo}[*4N_^bv_k]]vigʐᜣ}[8(l>Ũn栃j.f}]X{rWN �79r~9#wuW9vvMU_�JRT*8l;Z"�g)@%ĕsox7Mz+ߕz뮋i8%Q0�Zn9L26.}\82W~󺧟~9Cv)VTc5q~2p?"HN0/  `-Q, 0+v 6p”ᄏKbY}Yk'ͅ+wxs}NgGUpvN/PiG=˺~F{Fm;+�.JRT* we'ܙ /n b,RqxjpyAЬY!??V[DƊYqg:.dnjׁGNO@~s GX^{ XΫVQiر1u\qof]wN8!�̜Mr~0p46 6̟=yKk  68m|C X@؄o͑GPז[lokf]l(�|*w;eW"�9`|gr\_6 Lhi#1Hu9` Ÿ=G)�.JRT*Bh�t[.T]|^[omM@0Q ٳMa,`;gN1@ZqcU\d`̭q�S-k`rc5 XKŵlP?y�7<B}9n >jA6fg�k#׍78�+$fwp*c5F~Ek:cT˚RV-6 lhGʍj㞧n .rˍ �JRT*Jmik X-SIQ9 s A]s%Hr )>*~mk<M]� 2*G@+'ap-� @׎sNԂIs|Ñl\Ʃc֖Rn*`n:dh6( s48֎1caMPjN\ul7x˗hۜ�6+l�jd1O P9c\dO?x~SN�T*JRT*�nKKMׂ*,q8 Uts$0rlgqF�ɂ*ׁRǡG?pAG6Ob!R-`kl`N9�86(U @,Gcf k l~7[u[8hp4Ph:@>7C?z- Xk:}f}#D_>0ƀUW])wfsLۄ^5օ m@j[.Z66FN<xM/ERT*JR�-SN4K=2V&;HhˠWJ@ 8s0 r�d/ū:묳Y~M[U>h AĺG"Qf� r=4Xy\jΧw@V{8Uhsm,W@cR *�fy;͏~pa¾A=Sd6sNc <[طsc{/7-x6h5ns< |p<͠ s}n=̹6ygBJRT*JDvZ=� @+�0DUME`+;hp1Ŝ|s/T9`N. 庺V{*š`NS6Gܲ10 :ӵ򠅂Jp (9Zo\+g@rto\kmkl U k۳�º9sm]+6:Yd<Ɓ0l ZsqsAgծRpT*JRi�-u0#aT*(Ž�@`ş@F G@?a�xqVamH ivV/j{mm+)X/vmítl6/ WN+0Y-7k�<#5 \#X@9ꗣ ֛yvؚ`St U, q9;ٸ<kjW|hqY㢖V]*JRT*Ut[Z IJ Oq)N-(iSCrw \H0Y1? )7X ]: Bx3�93tT6p:_aƢ}߫%X0-pl=n Q Ɲ>cbׂDїQUf e- Lj_@[s B<zYs`jΠkյ=7c .h&pݿyrKRT*J]�(&7&H �'.!_@R.$GrA]%\GA+BpAs' L9yW88B<eyW,OWg<dGƸA(( (9 յT}9L}M 49�`QڙYOs=qvsjE^/7؍sύcvy6|>@\N/Y}ЮWw1&s6)d++\��銠 #7QT*JRT*(d„ ߇S-p䳂Q PfL@ lB9\ +�@N" D9@@@O|Zu_�@X匪0 8Q9c\RpνxDG�2.Þ܌%,}k3C5].B̹&oZ cǎ 痻mDpm_z饱66 8d„԰6@k3=wm"woʣrKRT*J]�z{9-�XVYA*Ա8B^e-xgZLĉ#O3cN1�VPg.*XTl F` ¸Te|&_a!`+ @;՞c;7fp'�0Ni k zbaE {_xV6/<8GmYzfW>f̘/Z]tQ=k?vp/jsU ldr6)�qG*�.JRT* w�+#w[+ʑ[3iҤ�MФ�(SFJ4s= ;"hԩS2'@Zq ? m e�6xG;U(ԗ8-\<‰l Xl%bZ6[_8ri<;g؜l"̜93/DG Z3@Ƥ� a>}W>cc†3է1k 6mZ3/N/\5-ܺ�-JRT*JEVZicf3@ BrAS V 4U8-y u?]!r9s07@ LF.${*9©EbGb)l�{l>'T?Ƣ_b1�N k] Z#/n]i(ܽ3{` T9Ɓ5;-0Os-�ϱ_ =<02|0 Z-L]a1!܅W;ʳ5թndUpT*JRivkh %ȁԬ/iYMsT$̅JUWq A. ` VUyF̹q jR-tX 5F AUUIa&(6cj>`7n Nsjls3Q ̹@{Ø<${&�9Y#lV_�ca|l6kݝݶ KRT*J]W�n:Ŝ Nq=Vw, /9kX``'āUR , K  9[DȮ~kaƀ[;k p-j.\Zo `9\V V ]k9`X;PGֈC52fk/\[?ɷv[ eqany'xb^{cAEmm-=ׇ?p\BmDa52RT*JvH�^\^]πQ8bJ_PT_luu׍yv0g@R5\Jp*טfI8X^y rB?[q)Dž@g|P0g+_RPd TrO-ظzיq\P*1OS1/ g^Wkr6V-Z�EuhMmx.y56WkOYm~Z߁TpT*JRi�-V \Jn(�{\Tp',A.+g8^8sh$WPa0Q:-WwqYJA1K/H&^c7� *ϸb ih4v� 9^`W7/kxꩧF^.?8�@ ˖ ʍ# =Y}ښ[o[_Y0mLʑu]z ?_܆IU�\*JRT*�nG`] &'Z/q'וsvgʱW@�� �IDATy{sOjZ..?k\;zK;ꨣb<`i})~7&x s RgH#sI(όsk<+>(j\\�˅gsy'O"U>]}l�-8v9T'>� 9ž�Z6 8\h}6 <WN<RT*JpWp;3�':H("pf\_` 0 ,9)98X4@ A+8SԿ>^N*P(μ_./` qu=ll?xrX|UR6gg s[%v1vsz1~s̙_trqż!x>7@u&9B}α6F+bkƥHX:;J%iko.͡7' Y;7>H�JRT* Rpnow#N&>w3N!g}n9W0�<G%` 70Gn%@ΠV@V ‚#׵cǎeŶ^a5`A{n׶ PD+%:l<@Z5_}'r` / >׿!ׇmpߞ*w9̺;klSfrͳYN�JRT* JV}� >^� p=3z9*s%\E.ϴ|]6Î)(�=́)SdsOW rm�N-g@=g@3YN/ :( ;U{o5V69\8sնgb,6mZ3ǓN:)ap>c\ŴmLgͅD}ޞr֏+Zb>=+X/.JRT*Jgܹ/4M@$En-Xorir'9 PʫT?e.pepK mnoBkryA<vm ʿ|y`6^ ¦rV 8;2%g0`h@:?<SbMPns8#m-5@6kOci9Yy60еĸ9f̘(,&$CI Z�3YoKRT*JaN;.wE)W{=أ _,uѝw&,PN/h\P I\ȝt X*ng͚pД#@3>a` g5M6ݤYoagY5`[T[Y\p~@-7̵6X[@X9�+\Y;�X{P^y8gmhoe#�$s=;c!t .JRT* wUt;Pک�žs+"U@ k[ ya@AmVCi>uݗGC:`3ό+sЧ/ՏB}Ƒ}'46 .-UY豂T 9t1[#ٮfl~ϡ6+7f5HGݚZNa6�`)ӧO3,T{7n\<3|<\pT*JRTp[ZfzR q\_ƑfO-R >^RO>Hjì(g`g)[*?hnP`;aيk cR:E05?s򙯾�c=6͐j9_\k6·6�}o�7rl}F;ƨ]g5_cr|;8&9r<@噾69\~^ns+JRT*J n:Bl X0/r˝y >r;GsQ 8]ǵ@ ݷ; vC`}W>AǏb!BAo.Pg td::6UXօ Uiγ1^7^(ll1?cQJ> �y28[Bɦ_�mσlm9??}:ЪRT*JpW@`.5\P)xi'Lz,X ^xM.agp#^"׀Uޮ{.X �kτ6ˣut@*6gPkrr9\UkƣP-V؊e;i"d9ûkkֆlZǍ78\igb9\f�W=7W㳞365upT*JR4*�nG 0믿~�Q'-EU{tsL0(&dZ&s/ sM\X0W+טc+Uȳj vgQ]+܀y o5v>�k l�k h"sG`m4AiUs vQn[,efΰM}Fa/Gלt߄ 8fCө.-}6 r󽾀r:JRT*J4 0Iˆ,pq€'h L>*U [cV$YCS%e 2}/.,zO1FF Hl\Pg K@\+뮻. A|Gfſ&#U06.Z8#8 zDH|4X稃P{cV+(i߻y�/Nr|;n vb2߾z~l9RT*Jarр�0SrUGp �SO=E丂dŰXril)rʀNH3 O6&-O'^ |rD+qZ -vYgsr 6pˏ|Z >]ig}v(k|.0 dY`G6]'ڵ`4{_{$Z-d惱[_.M�s6W쫂^rmL4)z[KL۽?oRT*Jav:�' Ą;9{W8k@O\Nz)ˑCm*g> 3�srM .\n?W-gS\b'tsR[_Pn57M9Ԁ 3yA6(VXJA^jm4xB>uxj'D[Wd>y<kQ,A`CP[#D#lXȷyN;�.֤߇1�JRT* Q�ŀK!&n&J@9_imQ )o O| `ȕ\IyBO=`Y;%'x*Dʼnuyk ]/q{8$ʷv[2N9B6� lS_ hA6 K. ]Pcn-PZ0jln\W pm �mj]'kSmnB,^uSkwuv~ :T*JRT8mi@`%U%*p%ǔC h `ܩv rJرcYs9B9`3ms~B38#Š%P~l[@L^p DRB9�zf86(W,K¥Ʈ_n|ruA˽!�-}u?Ͼ00O|ׇka1>Cb-m<h�^6�|\2׮O?�JRT*nGpV~EBdgjSZguٳg7w^#SʽuTI SqK$D ]N79s@s3W` %W,5sL97s5UׂnaWUe—x<:�cc0e= ]UhK] ƥkQ6.k gBLx66 TörzK60&8~cf6=h?}7RT*JpWp;jxNWY¹sT<+hk*3W. и rM+|GAPBt� a[ Ah5jY4sx50yQ@ b9\r?O6Ǐvr�պ u6Vcƣj6}V{Ժ'8n� ~-hkOiˍwhO}*BRyqVpT*JRi� 6pH�dsUx6Y!\XwQG[,Z\9Fa` R - Lor' : !E! \&}*hzPj #Y yɛšQ_B&pjm L50[?8�V9y n 7st ^pi}Ϸm"s.Ѧî}rt3I[h l6u d{+ܖ KRT*J]u�Bš8AVJY?.XH&9f*0NU$뭷^@WVwgfAhVDh\P^&ϙ3' AKc @Yߟg#</VZHwBO;@rm p5~cͽ['LGrsӆ�Ba�_-{ZY =;/o<?o̞8잉|eXgcіpUpT*JRiX�R,SMXޯ[ IEhN(sgH dN81Va0Y-dKPKYyxp!$oB>@?t >9ĀT{r'j'X_`W%es)S"=+Z'!êNo: *g/�h d40!i`{ 6uv[0fJX3#_`po|7_ƅq �w6(*gV�JRT*JmU�siA"8㞂:PL/zq:K|ZᾮTR N=Ԁ6߃# K(>r33 j0][n-x@c f9 ܜ��6W. Rs7tS+5 W F9sp˅uԂqT06暀Spƭ_}YAgptkeKZM dnn֦ͅj{.6 gx_׆JRT*Jv �,6 Kq~37[xAU�Ope�~`y'K ls:{+\ x*$*d\bPUA$wT>@A\^s\ka@ ~GA89jK5l� X|)DM6�8m 7(Mj]nk0xw}:ڵv qp Ce=9YصmPt_�\*JRT*�nG}:*†#\Oar7 8P vk�GS'p Dٳfj(`j Zg�n98Bp` %4I�QPѠp`sΉ}n= [XWX,a͠Tj}�Q@n}l’A+W7q;Fe�-U;6%8\6&۟s[FJRT*JR7�g&`U8)Lؙ/t� 4Y asyT> \X@)X HQ*NW(. j r}y\P ƜaǡնXeaB C6}8(GZ1p/5fts@gOέq _\c `w=Fn3]"A<O{Ʈ}0yN m_qiq [W] MW>x_RN+JRT*JTCNw Uio9昀Ƌ.(Li%pؒpZ űw\‰9H:#v^, 0qBH!Y�ev\\0my @ٽN8ɠ@)M`k 8܍Ѽ`bG}|աrPGq/a@Yko`=4Ƃ8pֲ뭱Mcz?m9VZs7/c V6* /<*TtT*JRiتBQ`Bg"#r&L9`K,t0YI*&̕ \q;BaBYUo`\s$z_. w {;a`>9s}*e,�*GOmnz;0 Y4w;m5GxU'LZCsnr֏ v3٘Q a�jmb�jm{qOHl*lӦMކlXtYT*JRT*WrtDQS[udPZ` af o H1)7|ssqC}E@ɽQ,TH.�®* vF>0c.H`'׼G[\W@ +ok:E;P + HB҆j3¼A63s@+"p/ø ? |f&V/.;0@ Zsy침ml(ZڐfεMӔ\*JRT*P*d€ORPfX0 2, YϾJ Q@w]yҗ"SLȅF>@�sͭ6 |`J%V ]6WrQE~n-XCx�0[$?iZ[/Ʀ@ [/jK@3s6ŝ,H]*JRT*�nG}`Gy$*/"p ,l4*"rtE�L>Ͼ,J O?|*{9 Ha 4Ȃ5!b/15t9@[� YlZ}/Qn6?sA5<W0empk&M@md:�R@W 6F?[ $oj3g WkϵϢ\~#<co#Clͬ/Z1,w>jӳ[UT*JRTjS(@V$`QY!(9΍yw}w8B0`G%%q#jt6|`~H[[y¬`8S/g >#' ,ԘJ h1⚂ec7X0\{cndO{j[Z)3FNsA6F0=7}[ RYy,lBYk X&mβoȳ6uQT*JRTZw,(<.9*O<9餓G}4c`.x ` ͞=; `}wT0vU? K�6+6 5yc=6X? Q:�G>ZA!Lo2XzsqY69ւ{jK0O `lbS@^9xO`F�=gYxm. tm~okfn9ZC}. |KRT*JҊPt^n[D!Ǡ [3n�� �IDAT� 8ih?ؑ� q()KqlXYP<u�O &`W.qD@jOv|Q, Pc.0o\k[kš9ap rqmJ.}>A{*֮_ks 3K6VY?JtpV` 6!nh?.-JRT*JU}+ `HGRVT3nӥrzX\@ 4  VAX֧{U%M`pѣG7'pBsQG:~蠃 G|rՙpdi֬Y0^ .]qV=-">~N6 9 Л1o6Y?ڬ- L߳�=<nes6�n?p\T*JRT*Utq_ x+OqL/@ +_| Pxr$(BI28U0`�gŰ�5j Uu9՘9`cl;*IV͵~{@@4pv-*l~ Y..5wtlɱ駟(˜&@ߦc6u& h֧kش]wplCK^9pƆg;*ХRT*Jaݎu0U Օ{˙vL[%W  xUah |T. @@  p -�J/>࠙{f~1NW�} رc카`tkhUE�!X+^ask<a*ټj`x =Gr'Np+=@ Э#^m՛gpݳz 5\`I&s\=KPonBRT*Jv/�n:N0ElMrCr*({_뮻.v"JV.,J,` l[ H֧bPI!ֽ੦+wU;Ow,{yT�+%llj9=֯v!xTV!A&5 k+<[1~`kg6l:8eժ9�m~',O<)pmX Y0 : KRT*J]�vm[Bu-E`8@X:Į ^Á\r9\MPDžBZ^`ec0op `TJ>,CqK{\_E oɭ9 >w|@pg_-`[_>a \K:cn�s &68\#7SHye!pM/P � ]knncm炿m�T*JR4U�܎�GxPՋ0\Q@ rTCs4 NA sA/`f*p^y g榺; !]LkE2 \ 6G^h_;bxZpNo )Ffjǚ'm `ߘE"jڶ]P@[c8Ɓ !s@ j-�uܶ KRT*J]_EHd�)T uF.R:yUa^\򸮠܂j �9Bk N/Dɹn4sW7g`,؁j J]v%ں; o/Ps.}@">X]w5@]7 70*4Wef v?u1XMe1cqHm�`pu:3̳u|˹V�\*JRT*�nG=s}iU5a<D&0EPPA^aX+R uvlX.Qa'X§9bΣ~|~m!ZXm접nC E5y@:CCl2B}F0Q!T.Zl=͑{_9�_m)e\d| XDZ7nmpzA,r׺X[o&C(\\۞>GXOOz }X*JRT*  4gpFQ[aɀd!u `DrDT: 6d�TX]0}&u@89 ^A < Yic=L0i^ v~epWGήq�]0sl}(H4VyN2%®[ɉuq0|ӸО K }n~:pyМt{6eqsK`.JRT* w܎Vk+ q<9TA7ϜC峂=3ԵWZ <WB~{_ptO%hos?B_@�P+`U-{Ƭ=a`Y1+,4xfΜm+ N?f„ ÷�*,fs+{jT;q S<׵_> Z~�y&NSNnO6-R+nzsט1c"?xɑ'-$ڼΦHT*JRT* V+G`pۣ:*�, d)>9 y.L /0̂0`lq29hkx[ Z"cil\<lbu?忪L7<Pe/ 66.�\9Wc pl]NT$ ̓ugZ{dCY{gtM7< M YaƍSX6�uq9 WBiJ!q x2L`N4_yBWJR"W�'C8H͘1#򂽶n3vl19FՋ40vl Y\ vjG ;Ԏ8 Sf֦v8 xhy�e:@f!lMQs}pDs*B՚k} qO z=\g3 bM�y>#QyF0h�X{ל8B9k,닫=p�giZ胡g͛2̹Pd<4i1-i>лh,J3]S@\*hb{oJu9tqxP;.�cʡ}q-Nspl`mE!@X6aJgY(gWێ4r#DZmԅ)a. Ї>!ŊFqS g!"l.^WB�?1Zm x6qIs9+U XyOq3a(0fw}ccc RTjCu7j9fĎK<Q*J[:#kK> x8ߦco�t|TRā‚4 X~VYXAdg`p^Pf,Vn,D b!>W V{ЧrJ3 53?@Ԯ¶ ^3Z8ٳޮ�N3G{}|ϥf` #[™mTpmXטm}<˂RTꗀȕ jT':)Jwwn)- MW\TP*T.iłU<0 c! \ZZU q1|` F^\[o5 UqnnR~+`|GPls{ν=P+ryU l|_c6m\؆k'L8F�2j{`c 0և�vgM*߅tKRLZ󟾇Fv3}OJR=�&O #){p`@=$ 7 @̜I0 z9)kAEh#Ş�'XR,/ڂ9 Zkp[ha@\uf! W4lAn617Ųvm(e<g7 enƨn84/׊ ?s‘]<h9W䫶 H"l\rA_g5V0u�3L<6l*6{E .JV$o+Jeӽ[hk?U@hYP\ '"0:묀e/aЊJ;a i tZ=}_ tw@u.RrA#Un(�kc $Gq�e3 V1�O{c k5\X .ds8GؑJΑ֎ ��Xo4@ֺCl=`P NPp kOy6<O\i@G6(RTZj B}S,T*1]J>:sS )EoUXɍ^B-,?0 θ`*Xei,*`ac˙}�tKI9]9`V5 rd@[ lO>pk&�x4chsu; vk%$ Z78@:Wm~_@ou37�m<l템krg!Yȉ6,|Ok[RTZF*#:}_4/iT*-^Aʀ|Y9YE;3p^=*)|fnqx l9LrB9p̍qr%Z8̙3h0b`{m8;8sds=wٷ ˡ5vU|rݫ,*&7];vls饗ί u4/`z?yϼ9rjVhZ-d+oL>k`ئGTst}& x=0g;>BoT*J#m#*'T*-uB o&y./71YM, A_N&�^3p `ֽ3h6`L@dNuhYg�q �E]+hyQD <}|asW9y>[=ccWAE ؀vt~*01r p"[}́}znpUȎzү9ɜ_?m[*JKWCQy6zԥRi1y տJ/b t \;Й"$K(P+x*Ϙ1#>h?.1OrKnjw}fԩfѣGX:nܸ栃 @-VN1QƠ0<h/^p {rZ%Pi[!2F_ré@Q}9Bm�|} 'V]j {'c-օm6''N Py |UyV_r%~�1�=sMtpT*-Qc^ jKҒ4( Vߜ,̀RɜTGio `Ooz@',rJEW_*f?r/vZ�+"h0<Hqvء?=oy|k\h@̹V9r0xFE5> 湱a[I;̥3j<܍6(a W!Мc=6ZSή62G޳'}*qwmNX? M$ye5RT*-R҈ȗJR43wYJPحU8sTX� rX)Hέ{r9"夂S(BɅ6giiL�kCY\Ϡ |39.nAd*Ze{'pB<gΜ,t�墂uϐh@Ug88Ǜ+vӧO 9~&hάYq`]RkGN6q-" Ђ<WnnMv}wUzg>\zzzJy歽zzzz�uNR9-)F5_^6 Y1T*-W GJV=S.)O,+s/AyA'gk H9~T-Hz@ O2%" &p Z/�r8$ȩi v^ Ρզ95.ruy:+WZH嬂T)/.첀eq_~EqmXOm< K+|ZhgT.pmȊuqEòMn66|Gw5\k=O+Z0cΞ5);]`kϺU�\�\Z.*�^>~4MB:m^T*`s+Ty?xr Q#z74; 6\b F9 _?ʅ @2-X'pof r죏>:�ЂKc@@<\%˹W\L]6�8Z,TZeEkXk�Jsok˜5~9䐀s,lZ6�<?ʞ|kG<Mf'-ߙ`+J!eߦkc⡖!xTpT* b8y~,Mh䜂-!�R™SggU|fJBn:Q=\Q@aN@�r\\EYƔ~I0H F^{0Y6L'p(f A + 1Hsa>̀3'Ys�'xWlƦ2nqmn`i6_{vpG>%m*JL#[,3iw;CWT*VE̙]Bf`35NBX@X1w)V4VS 4 g#h<̀Ij& 7 fBt4+ l|a ,&hmév�kr^ƒ]'>G 9n 9˜[iUKG٦u6>{ ']}xh`n@_[Z8`YnYɓ'�s~\\C 4xy]RTZt^꺂[<7M.JRi@�ei;n5Tev-�S+Ǘk8 �2W\b ; n\~oh#84nk}]Ьm\P04,syfp p>�/Bsb7?Sʹi"Xhs1i0 b 떏 I$S h|6{QGźeQ*anɠS+?˙̙3c}㾾2fkv1xs=m?k,}zX_@ . {{4l9})3gx~M:ry'ZZJ+ yHsNǓXa 1q+z]~>4b.#QgxPϵT]+?<Y8M7N#(b|yP]4W`UXުk9¡sxyU 2.'[GαG i9x{9Ȁս׾8ƪPn(S ��� �IDATG:.kr (\c_E9_3 ؁r�#gn!֕-oX { 3֎)VY6m[mS|cPnl ݺ'kZ.YA�"Xkt<&'qmQo:~YȩI ϊָEg/:!m;Fu<}|T柣64n�M?k~Fyi4Hliwi7Jx{?JֿgMœ]vNvB g+"�K&yJ{eN-hւw|rz8`L_f p` 8>9WNi�*<O�|}я{6Q.}Mn30m@k 6Շhձ*e38 $]\k= s<zXw_hu<iҤlH':+U5 vҋ^:1y/38tꈮ&06>F[#~d۶g7Եnz({3<wW 1Pg,괟Z9܃K:/KP[^j虜}ms ~S�p't4^¢8J U(- �{73 ɏ2-$C˝ twĹ~N%};,`b'Uh+c!}Uns͜�-4B,(m̵j Uȵ3FSrx%w\uwn]GrB{)s'ۜYX s}E?|:.NuVNԈ*>[_|MdsZm/N#X'?rsroX_ 6sFKpKb1P@)s,hOX'Pv,`VOiڒwXЬiLsc6M8�.Îd[Ŝ8�x #*ڸ\� @ӜTނU\7W^-' 9ƐׁW` Ayʻi\~k!lXzv!`m{_h2,9.ڳvY} ن\s�lC{=C�XUUk�AX/w+â|zVi(l0 kt`Py1,/`yX Vm? 1"]j Fg3m435VbE-Xl^CvPR ĀFp +ÜWqB1s=Q +rRsOq$Xs!,[ @<V7u8G-Dp:~ilޓ FC >R,\c W{.Hռ*Z Yh$?eQpJ,y OkckqV L1r|hιYOZ/ӟ1_=^}Z?>ڕs_3f̈gUݞ(yUH)y?PtC>XAl\KnA蜌k߭[`36;nSB!ut8~ՁidN2jdYC]Z("dwc6@?B;�̓묳Nc|-ۋ/x~X0U.>r 2 V R<Z Ǖ>䶂:n-=p �+Ui:c?- + H}+$>\aN2Jvc2Xw4suvV[mV�GO<fzoS |+8*ꪫHclI<3 ^J~ag|k EFZ70Vij OcAȖ "|o,4 ju+"Tq7X$j74KًAmŸ[m.ApTh@ [{G6_}T>]BBrTzn;sIAX\N8qqE@ tWcBH8c;yZ&1p]fS `ކ�7|rvAa+v-pk`-�6uYhE@:hlҁk\as ["T.[kn悍 @yc$d{kka,-k1N@3(RsE(+F 0ݡ�9+>Rez*ȃ┛zm8lmV(m|A>hr{ל~/_BT3ܾZ �)`JrTUx=]y� (rM YHrX}ƽB|$wT~0�'̉U5Zo9@+<[ 'ka>j̠X+վlg� ~p ͵f 8 +m�ěpo0mF�9Xt;I2/ eV{6�(圛gSl~6!�k}Y/{- n3yQZc )8!}�!v6`rjFvUAy]KC~S[X/JgƵ�GWEӾ߆[V�q938VvBl sJ�Z'aMy:9Ir.9 `5>�I.1u|P<C| 9=} 'YH2@N(u 8^rd y6o îh^ @qPr6�8כnim� v,"^th ߖ/-؜pfw7=ڰ)�mT|⴨*CHc;Xw":*m٦@Dv`XGQgy~PquLҀz CipE#gR1H$@^9WULD1y'!~"h  &D[}'  ;+{?5v3R;i3K#u"1RE4 4C bqs%@8prQR)Sґ/VWԎ47<sٗo<u_zo=ϳL}9m+wEi X^x0nq B"Nc@Z z-@`)@\+sR2wru�VZ/" x8-/5=l  fv͇PpnE ~Q 0G3yچiPDW^vk/64g5ἵmvM#ObӾ0"<Ŗ [tQ,Cc׶Z >֨ o�L( Y(U,j$%+tV.]`KwuW/2IK킀| K tgff T`!7\C.+yVKFQ;|RJ( +ho` 6ڥxkOߎ2 )�8m\]*v77l\ZPX H_L %\PyM;U =ހi #g1"üцZP8.'=徴,6ptZsM b=펦~.5m5ۜլhQg\۶LYF ƥg `�+wϞ={-I[B c;*ǮK.�MyQ�Zj 3�)WB� VV ;@.RZmwj^a nO1PSj  ɮ�]~,Yݞ 6^̭gb,]ny|o5Zm"iʆsͫS!~i|qn3lh=oLS)2sߋ!|mCo>?+}iQVBk,'�`P%/ �س>[�J5TՙZ+{-X>VT+rPk,`L!'b4Oʧ O618 6  qvNQŚZʨT]j,t{ @)`B�fl<ubs0A'bg*0t?u=H67;d)vNSޅ?`DY 7tSEen<ܩ\mn' y q768 Lu j~[hͯt9wz/y'5(HZiwPŶ^_>U{aMl_Aׄh>gTtˢRmx<6}P*dZn}Խ^?b>@!V+[ǼxJ D_^ xZ%UnhRK5bR< (vA-uYJŵ-T;]@'RBٌbUӮ|XJ75Q/� FUbja}BrxbeLKZB<?kܣ-NE pjWh=`q=nPL8ǼEuk9-?d -x6o k�[[Z{Q)s2yk&jow7, x\c/l7W\0ټrxlm�4c|(i0vl(NuR/Z\۝|7G I٥޼2(Ӿ.{R"|^!LN/� 6"uXٳVȲ~r,(V ٮya|0 ;.Tj "j3`e @7xԷ|]Uf &'v"̔\׹2sZ�l*4Hj6 MP A:>@^ *4?x)ryC cG T__+}^f/ k!{y~Tbc49g[UwچZ_ s�Osc/Vz߰izU�QaMh\-*f5;Zζ kl~~bVѫun}p { U=%o nF(DUt伂Qƀ*QI+F^T{ª3_+ɨs= DL}Sm@Ow K֏~ҧRڵJ j*S ؼQgISCō=j~Sgyfb<2(`G}F0rc3/G)|{n*NS ۶`}6ܛJ𺷣ۋx/~)4iKuԮx{skN˗^ƭ-2BRS7NFoicNHm|Y. ?l}+� �D0m%DEF +,Zn>O"Oh߾}` Pms:`HI qVJi{wZQY.o,.r -8p^YޫlOb 2ƭo,0/Rc{%RE{[>F!|^�ǁ4'ߴm)lq,x]jE3O�Zx:zm]mިBsPʂ^ro_Om1wֽ--tQǗhU1p&DV3CmXv<~Ĉ^رivFŸV_kۘ!kYC`a3q4V3'my7ͿSL)@ ^ Q�6SKm5$Q))a!H4;!rr; Ap^�ؾG_ `?({\hj &[ Fˊ}Chs±#Y/µ;T.ƨ/Tc׃{*~9啂J PRSoU߁43o sym(&W@؁-R7!R-B�j�F7޽{ sarA2*d;[mաҚ?_i}NP|rlX-6|mx-o#9I<T鶪sPxkA^B T&Dzh"j_ݜ_ˎMY]d:,b "/Д' z`9j$ $~/ṔN`PC՞8PK. Wn+R<A e_X`'*c^ A>TMj0f0S^h/]}P~ 1۫ZK> -8{j)|VB=6Uئ rw,jX(Áy hA_.1l0s߄ߴ!XrZp7vl g[8w@Ɩ9f>cBax տֿ~^?=l5�5ެq,#Fc5Z#�(?m,,z0g` R-@ J/%[ڀ!Ҷa0[`lzK%0`N" n+| BZ7 ƣ-@^PD27UJPacю�_= -67>I- >ҷ{A?_(|Y @u*P~,TXTko,=^ =WK- ̋푌g#7mnS>VA'!Dms3O6FepX7v gMxsI>ƛ*ư\ըu;�j+|wJ`:j”Z>@w g /8_*Y,RVV,4 @QX4fP ب� /ЮO[? r2r}rpC9vpi`?r 1i5w+E5B]5X|F؜K2�aRمD{>rPls!9OdSu-�^> R`ZYd�a'x<MfšC s</LҙfF> ۜ*e э·ڗiToV;6m*ŐXϤvBYB*P[Po)+tj-ceQ@%pBxҶ+R dg _JfJ*U( #(Խ=4:{`^q;ﺨ JGg]PiACT+V?rG ӵ`Ӧ`s/=4:t(OQWZX6Hh-XPЎE,!v(r޽<7EZM@{u /8qbӐ_; '`w^mk^O6ZLF,l[KjVjp܋%�o <Z[bs?OU  (q j dw ӥR 7w@h0*0\৐}ٲ0``<-n8 @@vjRl<o|XS\B).y@zZ/@X~ן, o5HQ*ڎ5&}o([0GB}%/VQnn6آKH7`7Z/BQ]oSo3g+`mYrTq?aNWxXlU؇j5[4J�\R AÌ%e;�SBDG v%|뭷PV%!{) UHR1gffJeem) Ee OTFW'EB|R)F(p |�� �IDAT)l< U817.C(�v،ĂR:nl|r |6B)Hmkծ'JpC[Q]g<zUWO~kCq.Sv)LFmтk@qcgOc!@~{>p ,VZ.6U'ն3޺BjG>}_VRD҆mˁȴXzƝn#�XoO#tYA*o ^/�f{# \g  @^m oYIU"y ťLR2%EWR,6/:= }C4c!Hu]8:�ƭ%kp `mQTރiJ‹�|s}O}� ՝rA`_x5g ȆBbAq3 tr͇ pO7 cLދ-6@1Y8t7Նfx񠐹, z s^Ͳ@dشpx潆 n<Eε\/�Sn)oT뮻ZM^Q 8$arxc9C*'L@Q.PSe hT: TXjeTsDz @10HS;¢Tiڎ\U]}vp`}M8h~Pdaj-uv L,!Tv.HU6Wۢ  [\i:Nm<~ 2P͹may2Ti?b~-j3۶hm[oclO336aK3ϓP{bKM[JCXJXLM3�;>[p K<ZJUH#0{jI1K%kHP�ӾL}(h w 'lؾ`,䩾LyTV'oaZJ{R 8]<5Nj)t=pח|gE a6Q jt IZ@s~7ݔ*٪]jz *ރp~R]aAqkq@{:@a)R x~ƒ_>|D^6;b]jrϘ058j/5IȂaU~,~lPG#5C򫈕m|�8Q@.䒒 *VH~RmD)~G RB)B|"h gaKyrXA|Y`/wwߩX!@&[Ũ6\C&N7 Y?wy3,Q0 $l ' -lAB΃^Tg?3e( 2�=ْ ܃ ʔ)ys3Ȏ |M[63Ee}տcmss$g[/_NxN;RЯTlP0փXki~`v k ]@tFh]BA(EB ar^J[ lgD=sK/|D=Wn1pTŘRzpph`}�ERUEG '5B)MVZ \39vjjų@ ~?6&Ǩk.A^>Wvc~cղZ0&F!2cQk_Z~8+%<Ux3Jz$O|;{`k#ag &|{e-C ڶx6 hqZ~oW) ^ )Tϴ(}ƻ MZZ6hP\Pۚ pPMج0h(:IS Z s`uKM8})W KM,6`/? z>czK;3f}F,!Ĕh�J''.TJ|(*Z ͛8>�L5N&\`cuI`!�=-B ֗ 1˜G ?p@37|׮]E6. QZ۞yO[V(BQ/!Iv||<q*$mRvkO6!`gVsb:6M.ol4a7e\3~[`1Kjc5P�WB ,/tE(P B6S6ugՒU)I =�s/HV_jCjc@/F@r6XV1D~l,Ru(A%dǵŨ6oQc(.{(˟Q_ƣs: .O@-@+"t@ňl~E+j92᯽[ڧ:S-\{@^Qt83v3x抪bTS,:GQ*?kUB:Xex2bj+ò p n߳t{^;h?TN>Jum[j Sfk0p x  ++(a &St~*S D[-F (@bmw]�MJuf¤51vjCLF^ @],�p BSH?=P5'V&z] Wf _4Da+ʽEcsg^\UkG_}GK=OmO_<_*D ?q!*Si+v/Tk n̞+ٔbl٫Z}ws j)@ u^$f֠6�^Z.Ti RL_X W\̙k.zԈ饍|m;Q{n'ڦEtKA޿cU p@�dt`PH@J )`XF1׎ j-EY^/ĺĪL1 @6 1@^ Wphj.UXOP(y>QjϠ9F= ~Rq<D�(xo}DnqzK*WS^H{c?bs?w‚1Y�y7'-$X<<mb P_e|hcԞD^;TںZ* $~PNlSV)a9mUV7�[ |^35[Y9f|TAp7o Y̷ V5ׄ'xu뽍hXx lU�f� Gp(B tFzcڱcG<*{( ƞL%`\YΝ;N0F\~aQ FA _S Hz49¶P�"X$^?A9w .OBe�_?|pUe\[El k6򌨴#,.S,_encȑ#g$svbsSj簜%B_vh+'>%v/ӣ\_ ~j3S?U `s W} qߖUx $wk5{^}^Ow,T\941w(/zi=ښ�L+=x0�SUK£Œ@@L!Q T1![%a@\y�jO34*@IHB{I<` q]�TS+t&P'VjB%Saɮb^.c^k7VI�ո̳cbYx"|3;H@v)pmca5cm(%,=r]Z#ȑ{r_,AFZ۪¤۠!v+f?]C՝m׺XRlc�U.' JfVzS{/mStXGo;F %,[ShmUxsQ+ g, %K]-z@š{5Xi}�?PY{EAko�;%p*|}_WB|+(zE7<coȿu?zIuQygXʔa;P؇Y;HC nO`X~h0wnnTe͋<gs)ikX?_d^[3|-SS6%peg ςMbF9{s-m�5Hr6m;!-tt3ެfi57#ͨƢȨqS [لD~L ~!(ƈZՏNw~h(>T;F : LA Ҩ/W20R\IEP޽ rJQdq0hʳ3 ֬/ T�깸OP>!Z8՞c Ӟe�QHܩmk, _!-9ӧiboa_8n'`qmoQNe<$V!xRAI?[CXغB[+jaa`h:^:i÷ouXVrvl !im"�0�:~}qR@u-϶BqL:0Fj:͂fU*�Sp=pA<P‚%N*pB)ـR`N(「`Qwi>ݠ?r~ǜ7ƶK7Y֮6Ey@ۂga.!ƠM[o'ߚ>{sg(~B;L޻s䟐lyS\˹~Pkpyv~Z^כ/nˁmyQ{ƑY?ŭx\EɞmSV-8l@4.;~ݪerdUwQ;U(kT�mE�(zRWA4BS0DQ;b{ 4*dE �"kK*A}AE +P �j')X[`mphiP:0W7uĹ V3EuX44g &n۶m+ۼLO`s=uy]rh*E!և"Y xb@[@˿,wZ"6V ᛯWb:el5c~r1r6 kXmxv b_G*|04ɵlgc>6]Rn K#;lHӂ5�%H.s?aHA'(ݾ}{截rT r L_o^PI= DIaeB;J�V{|-tV!`6'P*ZwH || '_~[׮~̝}q/b!\Zp0~Λ]9#333E-WY15„3gqET-N{=lC|Uǭp<X[H[֖ͦK{2*oQcBX)Rj8ǘ9ȓg5^j.~Ns U@0@TG DQBZ�FeǡQ$唂-j(dQ!u<4 x`hE(/wG> J(im;V-^UZK/p"AlZ1SRA9 Ui6&` 8 4mɑO=Ti@OOP�fZwE ?5mS͝c3ϔy|,dX0ijwntcS6LIiJ(dh1oOƤl-Pj;WkWp̗�[Ć �$0i`I$ O(L*�TS/Q99 _mwWpF>CE/ܲeKSe-@R@ε 8P_ Ek>(Qt �?}�G ^C!}Qy�Wc̱enMsk7( T!HťZTs'\Oφ+XB͡�v^jCsXe|F+EeN[6 5 ItfiX/?GP<i"gJv@bKǧd\4]ވ6�BN;!*�P@W]㏗O/&Ku (r^A%P``Q3`|r!%Wީ`[$  ,Km; Kq@ 忾ԂDܔX�K�[=ild0}[�qSڻwoI5GR l\\_|0',d n(JcPpp.ƨ=7撂 Vl~xVf0k˂gmO?tYP[_>QںcO R|f/I*UM5E_*o ^&u!`̗*<QV;?>e6-^;nX�b0[*P /,`#7q1ж8}Zy Yv)̩< .`sMKLuUb@7*dlODl~jS` 2�OyPJrTn�* V/y`-0m>9Q ܫrzmڻUY8 Ak zu]?>_B 'G̨<+6(<{fE mF14snA/�9goL!x=۱!|)@KK$*9Gߦ 0Ti)Bpј uky3i) &wm~Kw867"諩kKT'V`GĂ$"V~lI �H(OeXXN+ RLJ)pH±Ցb@l+k@ h[)bE9jŒ ,x#_As1;X̃vwPl2Wuwh. <X,�|Q/e 6m*0i\Bw_pn4=|7r]cAe`ms7{ Q؍%r85c1П1UMzP1ƗI!MrM�<E*p6aAҨ cnB<_POՅ o!.jLBFQENosTӺU$BW0bJ!g>SS\HK)K R_F+ b?\U+vvRs4pTP8-̙:J!R'�t "5Q)�`P{V#`yXZ*P't=S%�Gް&S-[PLawyն+WW|ݖGh}{f ,\\|Ewkc+R\|^z{キ|6T6Ǯň??-�mqȡ}}RcvKœv6>KZjaWOH{̞6Bd9[lL~nhP;|IS9!KmSc2Xǿi^ۨ6ȩ+[M0`G ^Ga%`C@r(vR0`RI$J%L AM*B W pZP ,oU+[�`ׂPoX@x:�Wj{"`1V)BlV5@@jH;oՖ~Au6| P3O|q+ȷ2PP)> 8�� �IDAT`P|-`PU~WPiJoܛcaTvσ6 ڵ^i\pɅ6(Xukn_$75?E6;cL2L=wR"'-Sۢi 3YC^ 8owE S9m)u> kKmj/2U[?Os-W p"= U kv \> UE2 @&`w[zWx ʮڢ"_x3,[|c+0B}(D :W :GKU4~ V@u º?5`SH25L*Ռ ImvEPfnזY8d5B@q }K/�g{LP>^=++_F |ĉ ņ'N4!(;ֶ϶Ske.}uuiBu|홎1J4@wNHNB~}ſvPi_uȫ0,{C;<ǿ~wDα\{jLߪI-mm]Faw|&oXbO 7oKG,Q)``)~ 5UoE IP)ď<HGgZ AӵBuA_{_>`}:*<%t|6 kMhRj� 3vU7@r`]2dNsH2Y@g4F~۵Ḻ�U~瀾>ͩkc"ҟk=!WB^`GhmDN.Я (Jmj34,X[^{=E꼾-(r-%Z%o,! z7 u~&}iղ;a6-}'ƕizT'qwkZƨ`X?*DH *8 (rDK9 U/VZ-b_atT9p8`/-Kr�vQkk !d|m_ <T_j_,Ya0d@KFQ ~ukK^pese<q>kKվas�ܽlj-ֆBTX. P-L�ta͞zOvTx~' }>>ñv?<Xi b}M6M@.Gff*MɕV_N966[1Հ/+ϖzzwX.uը:s0NguV ƀ08R@N @ ڇ5@*$|�] A{-(R8= F1ߨ> U;|~~+r?@$=@(֟q~ ʴk(K "y5ǔ`urmOb.dcs(D[1*�Ů<X`>Asm1 ~w}o$ߍOAҦjՐȽ'VݥQ{QIX}gw@)k)}M� 8zQav? EP X}, $Vt?x z̽Bl)@XFh Jx5& &F UW7_7xrd{ 0qo?~2^-V8ZMa2{/[U Ҫ}9rt ߹sg99Z0�T@ 0Pެ̓s~zA7W,-VgC5pU)ٶ%Zֵ@[V{)ؑ KO!/R"耏T߯~%L6`)R%)_E@@1`S\AcG)+bj ){oD@Q, V=*Vzyrx#(k/c(؞܁Ds#_,~h! /g-"+> 5./_uUXL}Ryͭ<_\X5o86oq |S-LhGq3bel*qgV+޿j+�-*c-TNA �R4k}K_*`/ru6U3•U{ uWh1Ӗ^ ]g ]N】Ϡ�GhT�d/PQA0k@tM xD{ p͟^‘=㴅`j4[D_�Rn ,x>쓀o۷(ͷ~{kǎ`[Fy&Z�28------mlZ?mZ50&jT?\_z#?\g*5\$8@((Q؍h&B �Y*I;G_MaBsOV<4~#)J+Y1wAu<<wR|> vA:X S J5F`q8saw\q(^ lE,e:N� 4@h`,8O9U].-,XDo1Tfs}J-(\i-/dk[9h�;fsNS7JKKKKKKKKKŶC[T7v.z96k \{L) > "k+ QF` %!@L)g#a=P WG,<F'*Y;QRA*8l dR??Xk a,DJaTX"?0k� pZ@R4֮1v�lhsEAuK #@w*[(i`WqkGvvTc֧*^</�t<K㕿ko(ƀޱ)~/mnͅOHC.2g?nc+~,Y*iiiiiiinlZvLCL?O�*0jT�*|DiAx)x(`9\zE wLQ!4Xi9 / S6 x޿cj|T`ӷ^{"hu^81yUx6W%H~B�e`pGUծ ]/*1PI!B j/wGjq{⃜_�*NUym\"� e#yg?[kʮe2mÂχpFhKcT'Ql䟆L 5P-[IK-!A!΁c%\WH-{R)KAn zQU%e: A�Ƣ?>F>[DZqF!'�43 6Op� )Tks$4 A6OEsM\x@ <XU12 .uYXU>^XX(aœeS̓q5^2pu OA- Xy@?y 7(Fm13(+j OKKKKKKKKK sɶS`8pVO?t"J[L$E`EʩB ]_|q%0't۶m"\˷ I=-P j 5@<˓]~o%`&HLvm_*ڤZ֮)8�RZymK x(`:B5G+ OlPo^]#̚n�9Ʃ@YlYe.%߸,, �y2??.mRy[Pc0orUPЯe,C֩mm_@ΦVڄڊ� fb_ x㍭BpԿo�V(ñc) < T)D< v^(VpBH(`_J( :*~TQf|e*,5( ^xA'`Oʽ?Z,p?LmBAv-ƒǀy3g�˜A8`֘({>*5.4|{n-OOPMhaD SJޯ%�֩m#EV뿟r4mhՕW^Y5ڵkW e<Q,R-A T\[�Ta@ׂ$@cŰ`rg`hX J,t: `6EN)V=A }WTO: 4 1'؎s|{"<0qOʱ'2A$57擺 05V3^}R`)TXs'$9|`*pi`+ϊXne,t8T+W Ll)~ Z2�_}˸빻VS\----------m6,|5vJ,Z B n<-ݻwo{\'Wu0y Hr? Րb(4՞?O Ȕ]bC�(5M`/*MBsR2 恤TՌ| V)Ѡ=dRgA-hʽ5 /�*05.Z #gX&yO3؂7-Q6 �̞5]i*cq@x3_,X~jk286 _Qla\?UÕ w H7tS,DdoZZZZZZZZڈmy MoBT�WlDW[ ѵ+5P^c^y B)Tm߾8gbDA!`F-oVdR5[ ,x jT/8Py9EcM0l-)>g~ Pў0_`c+njؘ@pka.\D%jO6Kn{R{OBykǎ'5kyU`Qm}s " etZZZZZZ:G06%68j5Q) �!(>z 9֎Pa `)ZG +X_@ZK-a?sXnTQPw /Bo駾n>!pQ :&ڵ4;.6΁ajk@''(Q9w>|pQ)\^{enu^;rνc .( mǶT=͓,py}{τ~e 6F*6"g&І-T~N n7J� P0�U0w)B׾ A8ҠW I~@Z „ j7*.ԆZꤽ:TeתP@ 0) D9 {^Q]CI{XA{l(T(JEu(j~R]ـX7ZbЪ�f�,ق>J Fz}KsHǀVReO7Zɑg<ZYp3ү<aOh@xPosڍB<JKKKKKKKKK =j;l۪� بwXʨP Ԁ'Q81Hh 쀲٢(#ࢨj`lmD ^ r38sS/R`ysshO\J*?Pv怚?8*%au|D1c�ƅxŚ(Ж 6m#$8ng@S6A9Ԯb?/TvmuW_a,<G!i</ ̇횴7Byp G*RDx}JKKKKKKKKNk`u /m%Nm~OV;GXoTNRd{NJH >EJf;_~�>я6"gr=p s} _(KtUh.HJ+EmK.w - r zAy�|<͉@+�:7q*? A*67RUT6F0 R*ߘ2w-<{93ce 7&P4&`kKeNKսOyX<{`y?pz#[ko6m:scMKKKKKKKK۸eت 0lH P*R 0'0KJ�(LiJLڗ7 мdRA b;#)$<O\q�, eM\dh} A6 rlp ‶> K TA6(E-U: 2W[ha?Tmݧ}9`,{-0Ȧa* @ h♘�{nG2wΛn36P=/�ϱnZZZZZZZZZZڸmU 7H->� p^E9'<r l)^ziHrZ\m)rqR!U+ ]r U>'�r]7 JѾk kE/>pL8:MQ@1u`Iu|i/X%%4ٵ@*M귶U y" [(Gڦw`Mi :n< T| šꮺ7,DJ�iF z tZZZZZZZZFܽ` 8RA (s)FbrU� <Vyڒ 蜧 <:9Brp;J/�|:W-]vص0ZΫBS| lڡ:B�h�uTި @2 i_q)hP 5T.{Q/2ǯ1R l [3?AyfpT =B='c\>OzlmDiAϼ݂@O+ [[pZZZZZZZZF:b4�`y9rBAP0@0%5}ub:)`Q2P}(pc) 8 R Uob^_*;СRL)ThDŽ5p2'x/5Z.-FK|f9QL\W°@^u]Wjgh E6ɦRsg^P6)+8SsͿ缱g}{-hB-׭]ЬIS� yFv}kN o$3ʟp[!R %d{rQ.pǵYA6 .U �e- �b`GT9}R* cOa>!ԀԁlHKAv]tN1ERb C)v*A1k+d۸x(?[hH`>̥fAA<Y@pΘ9R`ܚ՚/38:닯|[𹰐6ivJtX5@hGjV*S<WQ`)BA#eN0mK":p_St pXʧU&f@@=Pc Oa` ժ.z S@7UZml_‹ˎQW] .og xbXh0g@T[f eGͧa"q ro^v,|xar�wa߀<`etZZZZZZZZF c+pX/2J%ॄzo #a^ ਎ dB X*%lB @F9v19Gz?@)R9C+xb"P Ri /q̈́,!֯-\[ PKQ�@�NVxb m <=@[M*1Z\ Ӷ׭_wRy4.?ؽ,ڳG)*0%[;U0 e�� �IDATeA͘c!x̫kjZpZZZZZZZZF:vJrFhU�HP)|d  ӥ60GAuV h1T\-}3sSx`J'ӇGWU `]~T|HG,X䣪�9nš]LcW4P^ pp/'[HsPm)~}e#ט@2?̉?+`q�>幚?Pl B>Mɾ[Zn³-:Tvqۚ� ^RL5bš)�OIFK5PBc) 8j+_JiGnBnU : c2OR?TT3P۽{wPT)@OEk G0A0h*R^ 'OzpH9<]@mפ- Rj<N?�N,5 M r)kQkz ro6?Ju a̿=2�m ;nQ`K+ HѩFU񴴴0!zС�|�0XuN-2ItyP%Y-Q˜]sw<ʓ+|8 ₵!b@0 @-P &G}T{4S;AulE� :ؓs^+ǩ|BBWT6>c TY3 ˘o(@9PtF5B-.xQwG1;̡bUAO /ujon4,gpmF(6Zm9iiiiiiiiiiiibk� RCR(?tGBw)`H*E *H{gJ#<R0RE2~�tx{iS!(J/jZ<j\@PWĠ_¬2[ȳ{4V`T-� _$竱k5l�O ]ho67@ xX,b:mlnS.[ o(cJ]njǽq-8<kʻkgpZZZZZZZZFc]) X`B%F0UP4b Qq@c�% T:4aY=nI(5 t+uUmz(R}p (T\�o@_c 6v�fcwjPdc⓾gm[ p/ $8ֆ) R]/@ק3bO@~/ղm[?0 UɏRۊJްnC(hrȍMuNr`p@cp K= DP|cZ$Y@K/ri d aCkA/QhJ@$pƇ2  &;L z@L={8UZ*~h  (x69&o~0W0n< ov=ckG4)BA1suVXt(~>c~Tx@яy8K. 0c1.`yY+----------m\S4S.](e^(@S*0弐Y F81`t*`iX]PTjGW1F@.DJ ү6TC2xlg39rlO*>&d:q  m6戊K|‚1_c# i>m@ Sw}wTf o~�9<`)�?/{nb[?)|F ܧ_Sa7AS+C6et: (rpE*@Jx�/jhUb$ qw^E󁺨maKmvϞ=EM/ {Oq ^ 8pa*;5N) h|�w Z\15>p=s(A3ؽ˵`LKKJ6esDW!ڱm93ڳ =Tl!e#?AOg}6I|Ɂ6/FV : aXgqqYU8l] ɀ=(@: ´9�;1k $Ȥ@QШϝ;w"WC[peI0M=DR. *[|t Pr_%r}G T^~،W!�z O*ԀQ) yRm}*Jq*0`,ZTXJ,ܚn26!Q+9nUvXO>gk8˹~Ҧ̹Ms٘KYoZZZZZZZZZZ8k�=p( [vւ=(R8) )¤-> 6*,`lT``"< 4͒? nwUB| Pc\a~<,WW!-&kbA'?R͇>eWm,T p R.]!ߔ_ 0Ԏ\h@OWJ@< cO7Y.𫯾Z*=*y+?E�UX~90>k6b+[uD %#9-----------mR"XށFb#P!<��*%h_*"x(S?AUly/ Uu@TS?K �>ޭ[XRMȝ]y^O p)@S>-�=cLvM+xMX~* ?B PyʯWŽ=_ս #]G B5֯m�qCQ-n<Oqʵac|衇 X<|d iiiiiiiiiii㴮` ~'hRa%0iGS')բ = F]go` P)k^�A(-ny Cr<�.RH� (U<wR6@*�P`ή37TT7@oj yTe G^�؜ &y˼ 'nx~\\w>*~㤰 6;h6@4[Р>[67~|&g>W~ `VZZZZZZZZZZd]) ̵ (mBAtX,& @0Z~-DUy`^gKXp]�ΨrR対l?$dX@J= +_|SRo: ) A"%{* PB{Sϱ147Sx3?&GksZ0j}Siq\ os@0l �^ziYBѩµAk`o R!Zg$sW]uU#a"s}&k턚PCPg+PBo|z] (`Obha^m.Ea*{PLU) ;NQ$ƭgl-%X> m3|{Urco`KAH )rp-/UA^+ Ġm.)}/[׷kJ+m۶̻}~.?yQ8-P:[<Жh*?:}/:P#23NKKKKKKKKKKKJ�TAoQ12T[ .Xz,li06A"t\5bm>7ʢ0kŬ@0X)QtDJ$hnR+|Sr;aTUfzGevO~•Js/m Zq,�-^cǎrmCŠ@UThF^vz W*.S \ٳmRx-:j1Ŝ ͇dOb{# Hl %-----------mmP_PbZ@S( ҶA@5-t+%�z ˉpAf �T{ R2a®w0fFMB LDBe ?Q>9>ۖJxA]*ZTBm<`zpmQ[6u{VHJ>]oQ ol悒{F9S)^}w xs6�j�#JXnq�S5dPL`*Oر{©{uC FS] P `x1Ik€+Bx]�e~SK,�|(˭ r*SWSc)Q:Kȸ{\ -Q$Oy n QtA%6Vs ʍ~Zڻ, hL]EVMKKKKKKKKKKKK[w�n [>` nTDJ (`KEUք] 0@zPjS*(D۷*2Ֆ<XЁ5g}vWP(14?)|L@Ky6w[hGug[P 4x~�acGK6nN 5[;`)TlʴyV-PވJ.B10jԞvm{;g )m~BֽoZZZZZZZZZZZڤZW eW%W)P,R)Bdl= ?VURe,z}WK("PBAv5HRAm/ R:Ru 7�}W/RN)rb]E,7�XE /po> x68ma6F|)"NI83 6<9&ȣPk $߼x ,\y啥J|/ƨ0>i׶)--------m[nTz !"?NB'R&\`0hRcj}a˜)t C w\d@Ч= C[1[P/T`P8Ax' #SQ`[(DG? -0S~/)-�PJ.'N* udws J -F_2mB[M3sH/j/e+VG|ZZZZZZZZZZZ$X 009^~ .ڵ(G 8 EZ ATJ#`Z >ũ,XMe푽q�V*ЪE`׎4�J‰)WYP`,eo``j#* J&r|ٛn�jTaTEe y6Nc3/rnfBXH~Wm>*ʶ[QAypy27BUsWpOvN6Z 32{:ƞ�mtK  nQJN+!AYKQ�V&&5NLqRR& t )rx"U^`=|R9/sL/| @^�z*[TPy]-V[tRK$ԼX,m"B+YicNW0V3zG‚pwqG<>4}m47E7'L?9ւVpZZZZZZZZZZp% 6`IEM'KQƒ/'*8Br0-LZUh/!B ֮kFl(S2r=咯 *-XQ2 9{:6(Ȯ@Px1z6m۶_#5Y81v|pHu ŜBէ>vmOj,\F%�>̔d 4&k<sNd<oJן?ZpZZZZZZZZF:vJpТe!}KpfPDbw^[<Y i!ДQJquϊ-+p& R.t^-GavFRI <Wǵc hQ<)~CJmTo _g|[ViաU޸YmgŰ(֝;w~ mdN>0?(«}͙g  �4O#"Dݳ3ȍ\MݦSA>7s"@Xy/6 ; ``B01pFuU 4 *pQ`U[gS1`I (`(_Q}A*-j <0IpŒ}W8�GWRQ&̔kTS?k_()jţJlkBSd$Kl]6g[Prc,s7;vnmB8bdB=_ ?5=⭭ڴI!�0n}` H(%t)(}_n]yŕeb1o_@ }x*L*L%pd�4t͡C}R8h5p6Ť(`9' (0L U_n&y`V 1ٽK,0q(4|> !TqpV:gPe>ƣmA{(Ȟ{,ZXhR/�2!iiiiiiiii2-J򄲂n hQeCid`85V&8??_<b 5;KYaܧ~9g 9>�*4 Uvk )` wݥu4 ȑ#% /֎q̀!3NyڧR>(XTuV 4K \^}Sρq ny"j|/h".32TZp.\:nB3`7We�DS{Ϳ"E >/7* N�NKKKKKKKK�\N)l8Ns\̶BQ-m(ITMj*J|W ΢ j`?AUX?P(51ꯜW&D8Ti) mDܠ/OA-י}q0:=`bl[$ AbqX'9T d{_Δc -8 {Y@�! f/ȃ={1L=&*;!S?on*Bk6%�ױ+�K0{_j%E�j2X#{g_`ZPF"S+JBkb#sbV΃Gj5`Ñ,X`SbA"V�Ye޻�;ZS(°) Zs4؄mc lL5` aSm)8=S*w'a~<Y+dZ?ƳϓZv{[VyWP*U. u w0X7  b_.(�z 0P]m{kB䮶<WwL+⅜8WQQ1DT0{ŻnT4x71 }mA_L+8*舠2|62EO'á~ kW [{;[JtbpFJBPϛ7A!*3eX*4hϥw) p~P %@+5<J־b7w[G;^e'c>JQ+1֎ji*Ef2uȊ> c{}Lt ~/)[~_5> X,bX,ݟm1Ke{O(>O>,R �c .]ڞWk/k}1zv&AVj5?:7Wր8 &]Ȗ�� �IDAT8lKF5k4OLGteOUZA* /vm… [0lo3P0+%r Udɒr]˝Ew )T^rY[֭k},ߘzI)Ҧ{e@ġWX,bX,A)8 +}uk/IɔzoiԼ&ӁL` g_J+0=s۹ zJU$ cʦt*&XGmJc9G!Vm큥曭´=�W|nRڡF?%dPc@ʂٿZ|K ei˸(|,(˞sn2^q{oC,�_|`x]U}Zu`gcݖBie[0fYүcX,bXaC/^IMu&:RN QO"HwJ$ ?ؠ3@h )XΊT8j Rc~ Kֻ}R=esg2mJ\K{Rf/OҺ) X!-j-0>́JcgW3>۪^%Swπ3lE(*MkS17oX[�wq11~["XX,b ]q0@NJ2 \{[]eR˫btcWͭU.jL{vAAGzVSMTap*b `)|قlz$UXA(J'ؔUcXA` {u6H Sip/TJ'=r]A*) UDbw*U iժU=o2`y"0mWz Z>6L6ם-@oYDj桻-l+%| �bX,nmt֬YMJ*蠾ٲTZ){JtK.4 ~sjZu ]IA! O_o|Z`W?@ԽTLp=*Jm;4s 6뮻]|UIZ,@4DʱqR~ÞbhB8Y qT+Rnܹ-}[|ķs9%Ɣabl~dmj6�F]PO[۞HlCUy[TN�w)StX,b�polhŊp,PJ.]�K*D2Q<Oj @!.(jK tgT�B-Yp;�I)'EX9W:1ڢƂJAVM@(�(N%]=R+T\ Kςrc�L[+N8E1kO- xØkb?>O…X3ys??wq跲�Gl~G4~7�]̍g�p,bX-�v(`DUa)Ҙ NS5U *HQ_ 99Z 0%" J_M)�_T)׌" vvU6fb@p9H*yo`) f#+]Z]`m(:�芋RR=k/-W5Z ތi ؤDs%v?яg) H^Z5}oR(iW-FJ8,b�J붟͙kVj|meԹ5EUvosd믿5%>bX,zeC_}USۼjiO)@!ʯ`x+)`୎;L*1Sg Sa) tuJ4fN>dS=S瞦&/ 0L9sd.eT* OPJ_fMm�E)[^]@3Vj7[0Vž@j;">A#%X@>}O6_Y;4g7N)Ż s6b ͕k`\ai;]bES<;7\cbX,k;w_<hdd,EF1)P2;+RbI'ԙ3gN2FYTf{d/UW*1s=` �=� Q_zE= p9&�c n`B ܨh� V9(?hbfuN.,<;o޼Z d{|6� YJP؊?S)b9Sؘ,@_:9yk SiݝIu~l<{JiU5b[DZj:/%߷~Dmt:#bX, ,^xb̙#zX=MR#)J*0@'F0H9p\W]uJ`HN9Jշk=U|X@𭽧PP 4Xj3`J1Fs%�jRXťQ2v ,AJV u͚5kUx\{1Z0?/7)K֯1R Hl!/_wu^|8%fͯb_3ӧE g:<sma`X,bX_[�76pyM#֙GTEj F=JLRJcRm=*ec-P:Ruv JA>E9>|7 �>q_v._JV NS �ҀTXCy +EYLŽk׮mςb@h1 Z�[c A:35^W@ [ 0^I{1ZԠ&k�BLݸqcS6WZ,4^*80J?jgK- PE,Hի:,{،3X,b�pol`ʕCCC#G>H pTRpApȒ R ER B/ N Ԥ1q �e-*!]a,j&P=eofOq7,]٘opl)/'B%]f.-lR~A~x̢Et`q* ةVq F{~]p{5Cշ>E Aji6(h]#՞Tx~pYǓO>wLcX,bXZ�76pYgM|w#HIsfJI~ L3 ^%T#8M} gP Qp !)EjR([o@ n +e@~}O40<BiB *B@5JZ<ڛk@,'J7 a)U*Pe˚̘H5i*6U 5 a| B'6@|4oaq3F͟}oD\;PS,)бX,b�polhv駟 d?pTb8*JKU ,h %%lthJ e(R.)@ X�/ c ȴ"uCp{[g '$s �oů@*>} !s(ˢ�,S,\gyUJmM,[,$E܂Tiv튿1ZP�"�:�e3-K.m)5b j]1S});o*zKZ̗uֵ8X0A{ƍkTbX, tol`…H8^\ > h+UP%gl(HR6 pbTFMFg*:\\I U h bU +D`[hmP@G�eT` (rK8i5 *&xE}rj<S<A7X|vYpPiܹ1',\< ?[2(/G]5@/?/袶 k5b 7|q-X\3/~^s͇K.9sҥK�bX,֢�Ɔ?}gX-Q$) b:J%BR`{g0,}iw&<Qoo|H9u/wURe3.3ݔEQAd@vM7Ϯ-YC{ƨ_5Ү=^9%=+E͖ NAJB'ؙʀ.^\+ӟ؋, o Ŋl|gA0E~X�|�ꫯY2{^2?^ۼڼ)é0(~XA>r*XuQoX,bXZС:zi kXg4P~(�=$ �AfWX{\)C!0IE�dJjU>Y)wq x-NU*Z~@(S-k qP FO` p[<P5Y`1PU,=)lQw |)R7YX�bc8[/q@).�y`TӕW^.{ O5�ճ@_~oE J6?$3|G-Qd]~K/ �bX,s9gC�P`P@ʠȀ,�90E׳ P)Ui�9K,\�@Hk 1x]reXR큥0گo> 0}U/m.e ݫ'|)� {x"RX~}ST�&_Uړ jk48y晦mWl0.`LRP5>ҙ 4ŊMBb~>|q xWKݵ>Vj-h,-6x"GyK_՟ڔmoڴi[o �bX,ٳg8j � 8%�'�Q"8D@LpLIV RAȥ0Z `MCP@RZTVQHpOG_Ŗ THVM3*6r <}Q�Hԉ%hnI wDq.€d39 }K1vJB.| \W 3/Rg*>R͛ }:O=nL�{>,<Sm,L403f/[,�bX,[ �Ɔу:hXZ.Ց JV � ؔ d`pu.-{r @gv7c\Uub0 =XyTY@U{W㓾 + L5إB_)KXj)UuK~p6r/3Pg�qW|~CL~#P -$XlZĠmp<-jhׂH[I}on�Ћ+Ż$^XS)j |p~H٧DӸ(|Mo-�{&v}s=7�bX,[ �V\9?�_j+(-%\]E9Q L }R{F^vڦ(R:\\1ջkg=:gV:nUe`*FLg;/Tdc@*KG{T"IPzDnxm,~Z /lϿmunqRQ-8kңYE'b{-Z+0! j۾㫯1Rc1+`xůoZ ƪ[4�.!-^J9({̿E Ϛ5k/LX,bXZ@Ɔѯj~RP�`Xo (v�NJتazW -(2 \R6_~@4[ ##j?ka~JB A6F@MM`9\wH}RO?7`�3^1MU30�S{gvR1`LtacIjjߢW{?c qmesM�-řO 0ؗA`N}yAvO _q?_r%QcX,b}kQ{cC~g}6Li!TQpB`�: �k=%xIp^xl@QkAj`R`*+�F 2*KWb 9Td;sTc,qB048Ԏ܂B_Z/ Z P ka@_ ^/1Y5hUhJk*]?+XLJ7h툽XJm9s洶ؙ͑7k<b{Ń>In>Y�`3^J۷~{JmpM }�/v`fpmb̙{l�8bX,ַ� _|0JRr� 聭|oKZ/tQ6XmKGk  ̀'"DjAvU@5JPZ0~ڂR[@)` Цr K/2 M %8u|L{�Tg'EJI? 1�Cj)P_~yȯsv>mςahŎ =0onbs nl\R>gi?~l\6HMw �7Dlg�bX,<pt]w&U.T)`U Q@GKeWe.* |< xS ^9HibYؒwv,.@.Տ6( \V%b0~@ \�IR=k~6 Lv+Řjk/-A7 $qWq*֯1Q{cR0 &[@ 1/H63d~U_RA18|}iΞ_/ρj ]L\*8[Np H>#X,b�polhppptw]; 6"ur6"XD{t|zh7>0YjA'kNW^�dpXGF=4Z{S �[7L`@X|?8bY~ (�^(tZࢦTv?bkK5Ը+ \uE�EXH,A?`gB3ֿiKEbc>)aIng>gH#I*SEڧlx̓X(;`-}ϙS?G�bX,{cC˖-%@> 0*5R-ځ! kT &}|R j@>^`ʻ}N " oТR8 ՙg~�h:rgk36+)Uī|jf?prR\;` g`w¬gLdSbآjH A" 7nlmR]7nϛ)'sj/x{,9ɞhmQqg743+{:-T 0rOLLϛ7/�m3�� FIDATbX,[ �ƆN9߾)JJ%`JpTS96G14�: ?Y[Te"X <ㄤ\Ljf͚{> 45Ua_7U4ZE(*3/: dۂ%KZ#~(b}@!bh`Ŋ L2յmxGSeբ�0~L<@J0A )b7q3L%RJyb[i5bUU[L +LYeő>~cX,b}k?qa*J$3��^Š>T^g|S[(@ վS&q)Q> T@M MIu[0&EX*\ : M;j^|Ŗ>M�M1|ko+vn0@T`L �JJ`PAqϝ;]3}{K=g|)ȥMKPkn1Xg-[lNFJַwqL|XS`>b,}_w9'X,bXZ uYpFDtҠ ( ^{Rʰέu5 S SY@,]o y7UM`ٵIpGʡg@T+ +`gcOU Jw 6!?7./Vdbb 3RU,u8.W HJznoR+~|Vj` q~XܠBا4S]�(̀.VbRr]iuޯq �-Ҟ巌�<ڂXx֢XRy䑱+WcX,b}k<0;PyrA2$ TY } �d#P[-u +-*.P{J,u5 HK^ށ6 KjROfjRp(T&@�.KA4cgˌƶۿ?7# Z�±b PY5.#7SO= k闏2*H =3VJ]. Зn.Ś.;ڭ:҈r pR̓/3ڦ_qg[`߀/b�r駟-_<�bX,[ �VX1~)R_)3[l.8%Cp �1� vhC[zF{I1^KOTCq = =G^~p˽@}\>j" j1 @r쉥xopg|�~K { V[d L X3}lT*2UUup Ű__JK[>x1*9؋[12zEZyYzJ`,V>[�~kLJw}wW^ �bX,ֶo-z#R@}[3 :A ٰaC@܀dbzmY<+ԇ"J*|͛=&>K@ʿE63*UHwm˽שJt^J{6 ewm_Bq^6n̋bҞ{5״C̵yYZn;;Z!/@W1:_Ž,l;駟�bX,Mضn6v[3AJ�yj"Pa5R�EU[̔c;@Y lJOw*i}R)u~H +TL_ˇRQ\uñg{L+)nWw/@jSa{2<u,/ׯʭ4k 8e�G|[�VƣE>b πH_~_wh�8bX,ַ�m .\gӂO)R .KYp`Ri?(Ҟ`]EYm7z�.-W5誂_U v HVzrWG71NȭYtv P-A5;g\E, PݭX`OU6өؠԫmUG}) ^\Y^h l<ҺsՠScX,bXl[m_~mk`7LP۽?iKa.œ/ߗY}v<կRçKL.lɶd5]�O!/vnAumqj]{9iJ. PMMwoU Xf�xÆ c>lX,bXZZfu:ESx:xzT@��}l(�OT$�S=niqZ)ҕ-5}Q_|W^pLa2 `^Ix믿k=;bX,Z�tc\}����IENDB`�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������flask-security-5.7.1/docs/_static/logo-owl-full.png�������������������������������������������������0000664�0000000�0000000�00000071463�15110467414�0022272�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR��W�������� �IDATx \uy3XѶzVZ ڀ$r)((R1bR0RM@@xm-[(*Xy?-f9e_khh?O~R/RG?;mݶjӦM1gΜjhhijlW܃{キ<1[ou[fJRj`Ўsέ/x-p__vSܘ ,߫zzou] \w}yy=oDT*JflVՓg?YveTh,U@O~ת|;#G8W#IRT*5#/z]wUr-%,hѢjÆ խ:.p Fl?q)_}~rf&JR-L[qF<8(Z-[>WBT+R_3mIOrOYgUs(rYRT*IX�.Rg'Uys>)BTzԣRLt�K8r~VQƍo}[%$;g9>RT*J,͉~վ[mէ>+_J=o~dM%d#__/| ooU;s՜m攐`VT*Jmy <9X ,(9?`@_^+{G\1oy-\0wh__cBPJ*JR-Os/r| QoFX 77G z#MrNG@JG{ЭW{sj/pW_׋+ճ|fz8PJRԖ% V-qS>T_K 0EAnK_RIjPwݮζB`0ۯ'ކU5ϡ}\ړg: s=7'RJH0*JRK;9J@؊(WZ a~9eu@[KnQ\ A\rIO.�9KA9׵9qZ| ` u݁j @j…e@O3 +#ixoWXT% /| ߮a}RT*5=t�F=h\}ctas1#ϕy�I}s8u^~Zvmwdh/E7_Xu# [?] q!Q??nܿf͚o|pxY~};`NJ@a@N`U*JRz$Sp7U_|qO~k;㫛oZxq {* 5\S^@;TЇ1ڵ^[;l`_^}/c<#ۿy{K'< P qvi᫣?m=yӵ;VO~򓫿+ ?s_KRT*5xjmڴ m<l: JhL5*A4#.W<{ȥp x%w)T}c/׾! (IwyW@GgP g{al^{†oV8lVA~/IBkxᇗ1H<q5wIzG3'&k \9{%Aݼb\3r,YRM\+7?fT*JDJ[z•0څ^Xb $`P  `8\,mQΖEpsoɏR~vqnRJ8P"򘀞q9$h i8[V*8I{sT UjK(8yUD_h{y `5VV^ԳJJRTxIZַkC2Ȅ L`I\`q@Q-Gc!Pqnfp0Z8Hmxvtqq�6pfɻC&{vۭ\cNh.8u]W } 6euc4&p߸V 0Jpv}RDjopJR Uל sqA`c+ĝ�..yĸ^[B{Š!%S*X'pB 큓WUՁXb\;z\r&Pe&'p0KٺuJ= iC8z% 5,Ι楜y:=Eq>' Co*JR՜^3y[)!:s8G/ x0y[%Ѿ6l @ĥJ =9R3Ar\\55'smdV)/Z9;x>p>8q8_gBJ0JR`jN;me՚Z*�d5 m]@s�$ qs$.ˤv@ gHy\`E,�YSYΏ!@t�Vԝ?>'y™ K{0wuגK-иj2~ՁqJm ?09V$/gEH{=jT*J|ICmbŊ6mE6}.%&H*${]^ss` n<.򲗽8D68@�M[Ņx9]@NBRӦ1h"`r@M2s8Zѽ�&|9`KX9Vޛz[qc WT*h \~j> (iwIL Ǣ(p XB~Gy{ quE*p%rY@G{ %%?g 8'*+c?\92~m50O|!Ag+?XZn2ݫz+!�1[re8 fB{*JZ>'ܞj*^i^ /p8G/`JW&.-I>K_Z,+[qS9@�'�p|.gpv<1nmuQ ߟ韖*DAӆ'saܩ_;rq@JRTjUrιDΓ8L5p!r�@ (*sp\-suҸ8zq~kΕ?%lzr8N|&nk9[ 9!| "w% =0ꪒO47�f(, ε4Kz[`MU{0vQST*5NjW2U W, dΌR Hc<+#kK s<r8cKX8fU0kb_r.�"v 30{>hpȓV -&Q0aLcBPus7>י#Rկ~p28pJR UR p@вpH-.4&'c7 ;4H5| ^PܨC9,m56mPQx||责g“rToN<r@^\1:jGcz_?+( +J~UIs*JRB=+ +@Ľ�: )P::E;9@B>..chZ$Tza@kGwu �!#hCf8Rr \2�c`h2<=a F|w?�7.%-k_!8sJR@MtAf@= d�NZqgrqt�5( %c-a6 OMM:rx==ۮ@48Q~zvXO\]{% 1aI;3KX$C> {ULR@kV-ŭVO>D1 7,N:ix_@@fj@0q=H>)Yy]ڗ Xp2Q. ?p-I8PR`JhR_s.l~eEbE61`>uWSA+u\{{נr3T*J  \%$w sdsdr8:!=9`RwJx 83^K8@B– ,7BxZݭ()'u&@2v!49`ڌmeBP=ă6˹JJR@+᪾ ĕ3N { yV0lٮCV||EE>Q$؜bŊLB}{*Q;q]5J |@P%$ =ݪGoq� F(r<2F�Bδo,˜]tQ3(f5~JJR@k8@BoϜ%<x�*ibl J#aY\.A'V^]V$1p9D9t ( -HүuyX@Hx@Y(hnb0f+ 5s%lSy�-r+X /T*JR]rDn8@\ "D&}Ź@Dd!NVBtjEtA8L\$P$Dʌ r�pԧ>Uz_}y}Gvx|EQ6T jn^4U3@c?K~8a 7mҹJRԠkY&:I`ƒ{#I2gmoƼ,x򗀔v^fO v~,0@"tUӭ*ԆG>裏.u| +,+Q{` |A0ZPh00pPq�z.{JRTjԳU/�b<B9&x�JB2Ur� q!JR$&& Tl9 r8OH_` I? hq"/.F:aC}*xυlrČB@'cu&͘ޭZw_#/˸>e?}e<@Y*JRAs5�@p\" Аxw@sP[H *%7P9- q8F 1;`:ɿ:>rM7TBBjo$qWE$x.=ˆ5E彤{3gf\<zGdT*J\r0a(=Ҩ>B|;K(N򹢞�}ΕF'HA2 @H^s> ~�L=)3Eeep^r|cȒg!8v^xq9W0%}�a-*˷y}cV[{\RTj5H2Â�ũ�ܘs=�me8JBb/x>ӆ<qQ3`&H d +r\iF,!B.~:R;xJ͵ }Ҟp8S\2g@&1Oq95X>h'_ CLR@k‚ 8p@04nPQ@qO+ғnX� &'NWH8xLV:_ IXr9G*>)xpoyH�110f@g 8q<r8]U.Bhp0[ۙsJRA Uum6r)^CB`Q{ ( {qb=xRqe;0\q# Ā0%\EhS29VUjF@�?A rUTP@MqEs:3|.pf,5(אJRTjQB;H8X-h83i?s Y)Pkr�(@� Zm*Rs)ΘQ ȕh@Tm.1 $ [rJLYF>\,XW\qpM-@eL�nViC/+ciWaT*J -_HN $(*|f\lc#�(!9n{]ZiKb:7I x9/+ %"iMnt7N0A<^xF9aΣH6s 8@BjX, % <Cm8)|)�9YO"?ˋ[-"ݸ6m6laT*J fe)"K0qc@s�8,� p$DpnМ}8Ius/@5"|ի^U p\N�y00%d )ZQh+0c#icRГôljո򀐲+Gb wn,0hP<mN^F;tkKRT*e/r>'t$)aB C*!4%|'W)ڵ.6}KUҖ\)>s/}K Tn/#I*mFc69U/HЇՍ\+lӁ2 Bvbg9U4feCrzT*JR[Nh0tE <� J& `5P@ qr|sНTFޓ>iI*jDn"QoJѸwL4 7cP4Y1( X}3)0H1+`ӯ\/I#tr r AGOX^pe ۱T*JR3Wc:W@@ X� ^@TxP&R} xh`ť] d�*% g4-�_sLizf*c26!E^ܵ]B5�<aC@"9t`Hqvz8\�o8f=,}O M QT*J !57I zr�J809盆7i8Vf=ؒ0lWMQo|$D@(׈sGhaV+W~ d;e@ώsִaoCwmڎ5[ JR@kL@C8wJMP9n@C =Ύjqu@2P qN-FP桂vIp}h8NVE)_~z/WK;�U^Œ'U e_5ܸS܌AΕF\RT* U= {�,#ɳuB@*-%$3<Gcgm٫Li5 HiCচ뫺3VzFs8O@H[r4X┙h NPǕL{/aS^CY=JRYU HFKK^pRL;%TJ:�DnXܫ:Y[;�|DHϹ�  ?:̂v9a`.2ȑ^~L 3U<'xbcP3p8U "#t naB<'^rIRT*J Z6mynR@(rQ 4?pd%D\$@4̊=T9$I�Ȫ@�8� @nХ~O&S0aBP$GVyωaB*Р� KNHV74I`x%yeV[cF?%JR-W6W) $� }nꪫJh[�Pp)$`90bX6-sPd@g\@s8u- d=ׂ=& 6 %saO ApSx3VOF-EIspGyKf,pi~{�� �IDATEU#>}j]}gR85jB?@I8,  蠃*޼i<$ 39@@ ( " 09.Iuxs3(h9sx: *d,]c,?pq5i歏:;Kj'`)W}k˼&{Vuw-#,n0G4羦ڂ*sjU`E wo gGJm׿<&?֌8t\? CR;P�Fރ'50z0`e[\U{r$)8799ؓũ26<,cV e ʭR?e[v7 4ɡ %Thu` 9x_ ) Ѿs*,-4Lm[TUzԖ%?#b}ܦ^:WU^`GX}p](xx (]5 ہ(=^ E/*ąK\t\;:ycS~$# 9>IO*Ms̍{%X bC% �:αY!,ݪNEb4;4"a�.$oss=$s�P�o՜~� H3&& us@>$q0"gu qJ>S8OK Eb{8P/IU`.+ >IQ.VH(Y*tVM׀Jm Jakvqc<@ O.ҥK쓰RSyU)'rxc�q4I:?C Ƚ^>u\%"h!`JtZSeN@ |SJ`+% aӿ뀞cڷ)/s7nMOc1W:CԸ!*-anyR3T/hfiN!p80mo{[\�Gy@]0ŋ Q6X%sLNqȄ�\+.p~UW3}cƕRSK/pĀL9aEr8fB~.= [x q`6 # e&XR҂)r:^My+XP5CWX/F87+`%p@ 0r\Qn>U_WJh6LIG pŸRQ# EQ4f(W 7 r C)'Y{2!A}j3г1 8`g}UsJGӹo:.M fF]-UsB[`FGLJ8 #8@ fāiɒ%Z�)@FԜ6_$5`YRAy;ꨣ׼5չ[Bd  ])]ॽIOB@ӥm(?~2SO=0!`X‘6#Ws*M뜒9ZXT/m%//t@�"��Y \�.( ñcLN 1 +FNp wH邏NNxK\ثT$=!<si3⌁Ŗ<Ƽ.GkE ׵QjժU}`ڌ+mp0.`իTW-kxf]fa<NKmZ{8cYr� + j[ˀ-9.P0j@>(@/|8E[�( t|{3QܮؒD/|_[W^yeɝkO9S^ɑ26 %/ʼԾdfʕ%OJvЎUƼV~{u1T'pBXp'4oU#RXT0 zʽg$kP&f\XV}]wUfoZ!W`R5'HdbI\�O-(!1H>V]r%qW%9[^.袲pd  1`;3]nQ\ۯ0yT^-o ;rN?pݜ-㐃f!��t@Y RcjE^̂>Woqhhh3ƩV5m Kbn 8Tڵ^ؤ⺍cpoKmy5-ZP6֮in45[�FB*P) PK^9G|pO+9;dž%rl)Ҟ6`nGI@ I(`tkJM.탶>H&<BϠ�τ3oK@ dkss?QԸ]UY�(ϐ`LVkYպ6/mյ]߾Ily܅)c<V*v?_c5qhE:3)lOq} XDc<=~R|ǂ= 궇Hj5kt=MηoM~߹ݚDl9&[?ZMB׌NcǵrNg HP"A> p��I LsepWD/0ۢE% `|�WJQB<WKrWHO/{ X@^z饥O5@j�<+"PrF~27 eB&6obQ+#Ϫ߼zMIO)`j#o"˗E?}?@3l_;M 7GCN�@X[VrU_X{,-BMw=leà;x X 20�2�Eh l� ǁ/.!R(O7|&Ķw,NnRD^`.6F\؄՘ү2T &pfIw�GYV[hL@8!h<K{իWMPjS03?{=mfqj'_9eƳ6h86xXdQl^Y\C-P`q~D6l'k+}UU[BhK K˘);,ȵ)'p�8W$9@FT�g :ӊF9r*0VЀX� u(W%|.!�~Dq�G>rÄX7xcG޻*M(xHKYjtչLS/cLw6-V/kǪj]_S-n'0B+&fIв_S5SLѲܒX]<*A $$+)4:8IŽR]29 ȜuY &B L< w<pa`M'?T`K1P 5 �(i=p<S\10h28Wo~s+!BGv( Xr8`SeJce~ 2Q\>)>ֿݬqL⽞?MBMr-6 MվnIԣ)ӂз%PS7h0~C1o p7E � !9)8w|,]') qXo.AgJΒs/qk={Q sNt˝VAnv!s +4qά`o)Pi}M{/qPX֜ͪb*cWkV<d&ndWM�d\ё=޼?]P1QZ87GȖl{562ODŽ+_ (H8+_�po� �WQ;:9N�+pg4i/vBY,/.8k.'k8U2 [+ceH)r(qĸMΓ.w1$5\SY‚�ꫯ.C9}p5ZX05k5Z?"uW_҆ =_fCCC|~aY0ZNtUtщՒh`h}NL6E3E&c7)[Nha&eDs pR;PU_ �!?&wIm)'crƸ^‹{>νQAh\9W\- '2.πQ{@Q^xaMe\ @@+0hj\RSbi[T;FM~g_5pu)> sn&tC =3Z_קlj~BḶs) #GXh tO<Ӵ&g7Y7}֮SxM/B0${/4'd,D*9ED$v P �Dq!3Pma<�yK%a4 q><,@ ^r|fL\79`1N:Q_ �B\/c 8}]uU@*\g9Zi,UXcaYЈ olnӒ'V<-߷?FasW/ LȋX;cg%\S8e`Y]o?_t~6WWggZM~DWM~ m_pg$m q S p� ȉ @.Y^uBg x]% p8J@EBL 0Ø@pѿqw߽$ɣ] zڸ٪FeB}/0d: r8XѯyMz衦20uSؔĴ:M׏Umcj6v7uaawX .I`Z_1#46pBM f}KG)&٩e]Kl 7I][\wu%qoVwPN *؏vSW۪]:W`#!51!:BJ:+("e"6_^u0"e$GKNcB~BN\8!YoEC}΀v >.P  MrW<#=HT؅=?6�a\:^@R8^pr9}ƕ5 ~n#Bm+3dzb -λi&U=qӚj&j3@.?Ӧyt\,:[=/$.նjרpU�-I 2 Sk`½|C%TQ3�̀)zK1tCОuVˤ?yUJ:U{<㐐5 q� 8/a/z;Y;䓇K>C| q u 6#Ԉ@նMfݴW�ڠk[S.1E8mK2i2dGv;m/Id\)j2nOޣ'\,W\Q`mh Ŷ0`>u#'\BfD["$'K:{ir G DnC!<.`3F@mm:7ɸO9Pޕ> j{ڵЬ"4Em; ]OwY@ (yrToCNн5L2Q@EU7ԫ[\X ,akw C15]|OzzUl:�؀ך5k @�']˝wQGU>\)R)p`n@R @K‚XsmwESv3v~F$sʴMsr;!4ikp:!Yx46�hZ(}K_*qwFmivvk9\k urd�utjgnf\IJMƔp\'Ԧ&߷~N kU–8UE{Y,w >?tՂ*%s@ H^!pxkX T\"tq\o"�aj%A^z补v8(8jKsz+^Q9m|{fJ4D}=UU+5m|)5U{Nl !fV^Wj*qC;@d?:mծ# HDR{{ Ƚ @#J(@v�<,{6&hi{!;zIq@xzt Z8X>AuD|?DW˗//!H. 店%Gx8V)R*tz<`�吙Т0^_:i¯Cպs39Y-\P'Ou"hYSh] XMlY0ݞW5w[mծ!1�@4H* %2D})g򭜯uH i(PհPV!a5GQ-֕b,q3>|.HjԿp#( ;4sU(0h[;\5x+`=c4^p Dܵ,(ڿZ֒q׮m9?? mVfd_ҊipnV'w<kngfD1m`6]lpB&/n�Vlpnp tַ: 90eg$KűO; pE8%c \V$J2fG!@pĩ376P  gV&0P( =N8ObB@J9BƤ4{#J@ED1a?#ҾU@XiX+1]uΦa5H=#۾tä!딺m@70-gsᆭS xM[.4_,(dN3u%X7] =f[Z?P?nVWU[}+A(GVE9`@PBmQ2APMS\/&<:p yKq>G6m1(\y&D r@ՃP,p\}8j\2!Gb`1nH7^P9g*;ǂT_Z_w֛$^T/gb*7N6Ou&N[V̚ zwS`_V6^w>&Jcݫkt_ia?YS+\Eݧ(�ĺPIm+I�lv(a945 jE D`ႁ4=!AE/gL\Z%<;qbn^+5㗜/\*W?o|c29VI:[,�>UhLIMw@˄i W\ &j 63Ziν>:6 ͯ{TS ɘ6p ixm.hv#<%ksflݰ{Vn f� CBYcs@#PG]�na8]7"AfHPwr9"Hf\%7KBpym<`P-eN%S3c ʌT; t\it2 ONǕ2k/!{Ujth_.uք|QY}AnzL#><dGvT+4–M e9Vw.-o1`vUlquPSoM5 nx{鎎yfF r Ma8`|9x5 g p||q8>^aA[q(9NDG>X~K@+@*Ͳ1�1PNiK$<`5nmh)|jl\�#%J .+D' V8 ӞRj @G-HMXtmoV5\M˿Wuq~X>Aqۜe+zl-uGق._6z>9ߓ[z�T *4-r-vwհ.ZX~E`lYZU*ˇ[mU$`%k0\%.;� ̨(�� �IDAT \n�>+ǹ>O([ӽ?aGrRDkC5q<}7JhB8+9V8>Tge%*e<r~@Ԏ+Q*yքi~V2CM$}~@ HG~iS>q怅o~Geun<1U51cw߽Ts5+~a7fVK#� �  .( A‡)9X2GZ qԄO+%b>ǀpό;T$ MX'\'bU"@U9žsfkɛo&2И׭[WMo*^}tЪ'/f՘QMIhiaQi]rzZ 4WSa/R犼v9M_-X7MNQߘ-;OzܛYQ۪]#*� G6'Gqs$\>p@�]!:Xh;E- G7r$9f?0Hܟ�:G*A\0%}j˸v�9 %pZŸVʯ2oP crvAՁXB`u)t< Xcj2$GX:d5 Mxv퓵!s}e6Q6պ>~V˧xLЉvXwK[U}VnpU�/~�)GH/!JX^Bj c)Zr?{Jgr #gL&UVW0 88H92~SQ>|_+% 6 L- Gp=ⴝy晥$(* <2DBA%[n TϤ V{x /)}7>Qs i)S}M?ζ0Sm.6{gv7W�F&Ȁ+~s>Ǫ:N|APYxX�v9~P D[}0iP1NsC)edNn#fn�.C,+y@ [_Vn6jȚ~�OA$RT+ʽRg+|9^kKknBT}z]W57Wj*Z^L7h2wt?Otm?:7Y #પ72X%R-pb*N�W_]`c6 \q 99 '%z9W2rH p)� 4ȩr8N>l~ԏm“5:IDr8Xr-y? e2_s9ʵBԦJ:$_4 W/ƵtS남S]xu/Sܾ 6}f&O;6l]=&|3BZk{nix~fkUu+N PGD>(.Xk^*,=0U`JIpX5L+/93KNH`*%A`d|N@@"H{ֳUX&  oi$9q 8/׸7\1P*ݫf!kC]\tv}hq4aB0tj:.츼m{ {AasÏt߯{q<aǍ}nE+kfMCW릉γbڴi}UUk0�"Ic8%/ypi+#LV O*\Y2FEp 5rUy~́>s ֜DžL�h0}9):so$%�)%;*E,cμKK%w2+W_|qH qk}aj=fYVNﵧU,sU}@)�h/9pGx&=,oui{)߶R\Y~4Բ^scߜŁӈ APùK%R#D`BHLق38l�$W3$.W#< A*fwԝ>BQ*J%*=�]wݵ?r%<Z Z8&A2a?0g.b J2 r3ear.b sTN*KmcfiPgdjǫ{0~Ol y/l0k '+MM;8B\(!=04K.Ӓ%K `qp->r2�# q�ATԩ{@- 4iGXյcV%mh)�<%}U9eVֽ�{)N~KœXEy/\',�q',J1RT*5YW۪]#Cs疼 + ŕPq. JBl[.Nxl#]F{><(I& @grXY\h\ h�\.@rʢH2N.|-@~�H2qPZ/(4 Lq\U*JYc 7vӬ> |st!Bk$,vG� l9Kqw^ q@pZQB+ ^׷A^"p@R;HE}*_yn\H{ q(<?y\` 4Mηr聭O?SB>8/we<`u–QN*}_�?rۛT*JMbK5]e^>OnsPN=r/` OBj7^�]zWqs@__{9 /e} Zr+I.t)��qy?* JJ8u2 '0r8T vxn^ce�g*88^ԗw �Zr� =[M&(T*JM&zUϡ{XB;Ǘ=:NO� \(!3 .w lX=('ƽ[خf=(ui<%A x8/ '=`T&$!Dna7�~R-NsO9.˹ ^VF\$ GNY5<2Is* XT*FmMK3tSJYa ^~XP7FXN P"'##L0Ƹ@`}{_q8Gt\+(l@E[�*?zUu6Z =Ā.ccdK$/]t)GämI!Bs�VBv\rˬ`HؖǼ e,i~7EDA1}ϵdeX0JR3@^ʹӢO;UUͭ2 $xHNT@g Bh` @ɱ^ �q.p8a`̜@LVq 0#�+.O^:I77INnw[  Im4Hq��3omsb|C;:YUUs}�CV#Ry STU_B5,TV5# (# ` !<pБ8X@BޖPn#ԍ7X%I.wI/Z[ǘO"}.U`ʱ( ]B<i!(AB<s 6̄V;(__sh9󌰤q:GX0*5)JJRx$xb_\F@& \tEp$%qqyW�[lٲ%@BH;&l>p +B1nx1\A1NϜo^L0/$Ps@\PPT]H2cdYsXXn{ "ST*oXpC *m rH�Ia,b.Bj�8IrsO9B>Jkq5ה׎p#Yk_~@y�#,!@c ~4$ (-t2qcMHz5hEAUIz!HP:WIQ:WTOkP\G9@: ȭƈ3fF-!05& 9pq 8�$slc;MBJ$�&8MMIW5(U]r>1q8ҷxtM;g|J;p" [e,<E5s|!+1V ނT*ha QD(=j:!p"pn�&*?BfQLTƒBץ:P(6i.HHyUQF�@@R؜osL5{ x3>3.fn2'$9w\¢g|11~ҹJR@ka/wPա8�!3##)+_J '%&Ssa1`@P!AɒTMq@wIrl/V52`$") y�(;_WohܠI\2X&2 %r7;�7} O7 :@ WT*h $\p! ]y%W @� p B=IΑ$Tn%HH)s;j{NS&\rbD <]~p)%�:SV1#w%l@;Ƚ2&e  /is.䒲uPlCT*J b>-2E �@m)H}(sJlp�(H4AR p83ٛ1$K<Wd>6 ) x./2O4~EœҌQvI t2gl J9W pJR Uץj k\R=O.JqdB`X!8�R9`͵L1"O) 1�M\m8U{WDԪU +PH.C�-}]Rjv+rA{7|v'C!Is1&]?eAЖST*Lp:4JM<*U\N+Q'Q"AO,jdq '// ()i@n\-f$dk80~W� ;NDy�/Pt %u"kflLrpde&=r*6֗6a+[p#S\RTj5a^EH�: pTRp_W3,@Zzu/mPKϣ*<X? 8q%I~ M9`V3 .KD%ydjO& GʵR<}Y]Pҷ"wM>(3RT*5Кp%ŕ  Ji͖ +cօ]+!ndp绞C %K&R&|IFv(ɹ=\+.4`JQ:G<g@0܁,'.Z\ovˬ 'k,@M8BZ>NU׷UpJRA V#e�=Q V4$! XX9ӆrԧ ) &<@ c?AcRG� hȕfM�QA\/C.\)s</gyrф RJD!K X0!`ӎENRT*5 W# Ap" #Yqi,x+ HlV@+6P6 qJ�D> 9OsXԲj2+ Ҏyt Ci7c%@!s8\Q6  g?M¬n\re'W}r9pAkݺuAP T*JC] \XouW(2$'l�1SBh*ێu�:n' y$p#px/ArqBqܢL~Q2_~]o1P%rjJy[r:nQ X]gn#fՂLR@kspp'*@% Dr\.;%"/ 09DW 4|+cq.W 7ehA+`)9u$[(yͩAn. c+aEsK|>k 9Qdu ^\8en pJR@@@ԁ�EUV8R9P4CeE X i*Bz`GO^=�K<[9hҗ+t>\(B P^P8]zW%Oc̉exVF ܌;J@EL1 /*JRk+�Qի�(�-q' { *@2՛Fvˇ%dA\ԘЇ>T@y(eR&I9.<}!AmIׇp9T` @c>B�Eɡ9"k O盟6?ϔVI:ǽQ].*JRk]gGqD~%I: jtV]w] )Wd8 <qz<k׮-'<',Ia$N[p�xqq>•-xPkf͚IIdU# SA>ΘB‚;!AsdΓ%oc??nT*J  Z6my#><. %1C`Oĭ! 0|�Кp<% $k(+\(p|8eQ q@]}%*r;LP@Пvm_rl^wJH4W!QI}=(QzGBV1#RT*5 =K1+@�Lpi8/W^yeIR`sK%0tIKBr<Ȓ'1(q�%.@:#A+H8= rnq b){vaU=%^gQv+pҦ9r.. c{U�9?JR`j̰`U$yk<@GB@q!7@ pTM"7+= qJo{IjW?K`<qjqcKX0k7 $\*9Z)~j@Ѷ7�R )/O qq~.qQeX0JR F]nY8EPrDŽ7%^"$qOB 8cl|62 INWk(`Lr8CGܨJ )O ܀&Tq %(.@ Dn\lh @BBueX0JRA jhhh/Tl P]7�$ܛoԉ {kŽV@GpC."kHr\cٳ+m;f•V HX8_@I;C>qP$uyXxl %^<2>tRT*5ՂVk. pM@BҸqs)+O~S"4a8p!7 P iK@గϹR\,!7Ou�)qfܦ҆%T d\.A|P48Kqׁ9ڛҗ+'\iN(?ϭ>܁(IuµNZ%NU*JZ W ˗@HlL {3e ?jyk_;H0dA[��IDATa86mڟp@.x`1k8Kt2P q$;,z+ Bj_ITTŖ>q˝\|ХLßٟ:BR"~$ 0lnK6*JRʹzX)^@ �80YHNSDTrp㽕| mnp[$&(B\p~ yB(O�I>ؒ%K -Z~}õ%S=u 䖁E}EN8EM-Yq$[`𦾴{iJL̹JR@gl-oyˈo=:@5w `�gȪ>ҁwy%' qw= ,@ĭ Z|&&d'|= HP?LhF(IRnDoG>$}׃<37wn*aAr{ hlD'4펡g]tRT*5$jI q]@ITJ?r@�=`yQ]V {%d9Kⷒ x.&O 0Ƀ>V&ɜ.!<9N*s26Gډmz>LnN8Nk~� K/\/IH rԴ\ *S+}{?}WUP7תKXA`l<T*JRӤַjy@\&_@Fr: 1l$.V70�>sݙgY O<GARbL߾Q*cۤ WILxR>S@Ûɜ󶷽RK9P LJN7d6jw:P TAPk#ǜ ni{F=yr!ϙ3'ÂT*XO~r<0H(QP& G �H,'qU64 Nm#ljpP`9묳J7ȡ:s Hi P$ϊf% @`T&B-u ',WU3f,'嵰(xuUW +C-\<szk_{ܹsRT*5mv]w5W"8Y'T8,<""1u|&D8P޻pbPE  FE>C8DG�R@H5tїב'̨].04�,j_cW8Z<*}mz8ܼ,y^nشR?VU^s5a{SPUUsJR@<ꨣiqj9h0�. P}`` p+t@�9`s��e@'8�dx9P(mH<\2�Уe^>z(8/ucҿuc琙'`dv]Byp#SGlFNYͨ +Z\'?\RTj`5Pa?>|sl@x�*yGlB|B`/r�'v>Fx-Ҏ>9nQTm"SJA0$Xї&<r⾩mel&z8g JxѼxgL��/ �q\É6Iwqs׸51rQ qRT*5(:{7Pr�_`k+섽<H$ t0sš�}s~W`kDROyS~Wg~Û;#mr}�pq |9� $ 9 Mc1B\)1h//`|D.˘lDEQ<b#Aߞ-.p̔wU^կn9_UUU*JVs;\*  _DUP07|q]Zi�%6m"q"8v9`"t& /:C-�3�c `G_H`hV=A+0nݺҞ.(3o<\ ǕsHEv5&&}<EfӮW l sO6y޻v!sRT*5oc=JS�/A'XX3Մ+iy Aޕ\#.m^ Ѐ!;N<+# y\<sд b\"y%6YH;)) 9_\ # m{/ 8c<õ9uEs U2s=-Z(*JRJh?s[to/JLl~ \MU'+�h,hMUs0ĉn˹c#3-zk^S /#X%C'h#Np,lTiWyBB(D'4( $-�Y�` x㍥>k�~לkN `N)_0.W_];p=ܓaT*J *,;\`ܫ|s�E)ε^[`K�E=!,%ܳs6^\!$gekp @p'3f~9Wc~L ,;׃#}k<�=79I ԁ86eB@ @:|k+r'/{Y>KmA=y:WT*X 7tN+\4qi�/|rn9�yqN/p�vsjP"D1pkZDqx q~z!;@_8Låm\@NV"hl8sa`%il`�򻀐~8t'b䯙3@27,Js}f"]%tRT*Jmz͚5+կq0=S '=.w9g`!�"9E$7:ɹ9`,q8WNM\a4m\gn_p*"$=t>8Q2}٧AѾ8I#wJ(tUŋ{wxЬX\O=v/5o޼;tRT*5(JBo}ȀǸK(/uh(w�\(; qfkq 8AnN8T i7JJXx\]R;[o-mr|&p Oؙ1҇ȥ6:Z';=zڍs$sST*JmzmYV[pR${rx )J+,!9pgg݅@xJ6 &^zi3ŕ6 (��G.2 q:S�P�OhiϹNE2j} (rY#(0JBsOӽ6_ȷz1!C,Θ\|v-T*J nQz\��/zn ExJ8>J(2�`EB@\gŠNH}-(Ep\]:4a?\&+ 9U+1Vǭ+C|͟KPn 4g~`9�P7AyQU }§Xv?n~:蠄T*J ézvm7�'�L T ;()F?_$+( "VG}tmS\,.8x; h�+bK\=%\RTC Bv(#h)um K j6җ`1\*P焹7g}vbHRjJ1P?LE 'r894 7D8Vi[؍(<˖-+< aƓO>@�K&,Y:S/`#[ h8f~͑3doAyM 9Orj< <dims5VBG˼]ϹvFs9r欌^k.\JRTjPkŊ=CgWk`Oa=_\@*6b^P|r-x[R-_FbHз7m@a焄DiRsFSSԔB>+Y1< ƞEHzr l.'0$'N8(iN㱃89W(9Gn9; 9fbr#E@@5b@zΖ}CSFj!6*Np8sU*JjV7nf'>4i�Vk<۽p�@~W9ya=s-Ps@2]\ �+/q!{e?({!�)委iZ>W1qsrV+ߩ8]\.өT*Jլ pao\ރ&0ĹW2a�Z)ꃍ S$8r<k\{a?dkaY/�n Fq,wy!@\,pGM[<@T*J3lv*Ic쳱CJqs� 87~Rr�W# >f!`;z 9ֹq!Թ1@"nq}߿?+JRi& \5M ցwE����IENDB`�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������flask-security-5.7.1/docs/_static/openapi_view.html�������������������������������������������������0000664�0000000�0000000�00000001520�15110467414�0022423�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ <!doctype html> <!-- Important: must specify --> <html lang="en"> <head> <meta charset="utf-8"><!-- Important: rapi-doc uses utf8 charecters --> <meta name="description" content="Flask-Security API"> <meta name="keywords" content="JSON, Flask-Security, API"> <title>API logo flask-security-5.7.1/docs/api.rst000066400000000000000000000320211511046741400167250ustar00rootroot00000000000000API === The external (json/form) API is described `here`_ .. _here: _static/openapi_view.html Core ---- .. autoclass:: flask_security.Security :members: .. data:: flask_security.current_user A proxy for the current user. Protecting Views ---------------- All Flask-Security decorators are compatible with Flask's async implementation. This is accomplished by wrapping function calls with flask.ensure_async(). Please see `Flask async`_. .. autofunction:: flask_security.anonymous_user_required .. autofunction:: flask_security.http_auth_required .. autofunction:: flask_security.auth_token_required .. autofunction:: flask_security.auth_required .. autofunction:: flask_security.roles_required .. autofunction:: flask_security.roles_accepted .. autofunction:: flask_security.permissions_required .. autofunction:: flask_security.permissions_accepted .. autofunction:: flask_security.unauth_csrf .. autofunction:: flask_security.handle_csrf User Object Helpers ------------------- .. autoclass:: flask_security.UserMixin :members: .. autoclass:: flask_security.RoleMixin :members: .. autoclass:: flask_security.WebAuthnMixin :members: Datastores ---------- .. autoclass:: flask_security.UserDatastore :members: :exclude-members: create_webauthn, find_user_from_webauthn, mf_set_recovery_codes, mf_delete_recovery_code, set_webauthn_user_handle, us_get_totp_secrets, us_put_totp_secrets .. autoclass:: flask_security.SQLAlchemyUserDatastore :show-inheritance: .. autoclass:: flask_security.FSQLALiteUserDatastore :show-inheritance: .. autoclass:: flask_security.SQLAlchemySessionUserDatastore :show-inheritance: .. autoclass:: flask_security.MongoEngineUserDatastore :show-inheritance: .. autoclass:: flask_security.PeeweeUserDatastore :show-inheritance: .. autoclass:: flask_security.PonyUserDatastore :show-inheritance: .. autoclass:: flask_security.datastore.SQLAlchemyDatastore Internal class implementing DataStore interface. .. autoclass:: flask_security.datastore.MongoEngineDatastore Internal class implementing DataStore interface. .. autoclass:: flask_security.datastore.PeeweeDatastore Internal class implementing DataStore interface. .. autoclass:: flask_security.datastore.PonyDatastore Internal class implementing DataStore interface. .. class:: User The User model. This must be provided by the application. See :ref:`Models `. .. class:: Role The Role model. This must be provided by the application. See :ref:`Models `. .. class:: WebAuthn The WebAuthn model. This must be provided by the application. See :ref:`Models `. Packaged Models --------------- .. autoclass:: flask_security.models.fsqla.FsModels :members: .. autoclass:: flask_security.models.sqla.FsModels :members: Utils ----- .. autofunction:: flask_security.lookup_identity .. autofunction:: flask_security.login_user .. autofunction:: flask_security.logout_user .. autofunction:: flask_security.check_and_update_authn_fresh .. autofunction:: flask_security.get_hmac .. autofunction:: flask_security.get_request_attr .. autofunction:: flask_security.verify_password .. autofunction:: flask_security.verify_and_update_password .. autofunction:: flask_security.hash_password .. autofunction:: flask_security.admin_change_password .. autofunction:: flask_security.uia_phone_mapper .. autofunction:: flask_security.uia_email_mapper .. autofunction:: flask_security.uia_username_mapper .. autofunction:: flask_security.url_for_security .. autofunction:: flask_security.send_mail .. autofunction:: flask_security.check_and_get_token_status .. autofunction:: flask_security.get_url .. autofunction:: flask_security.password_length_validator .. autofunction:: flask_security.password_complexity_validator .. autofunction:: flask_security.password_breached_validator .. autofunction:: flask_security.pwned .. autofunction:: flask_security.unique_identity_attribute .. autofunction:: flask_security.us_send_security_token .. autofunction:: flask_security.tf_send_security_token .. autoclass:: flask_security.AsaList .. autoclass:: flask_security.SmsSenderBaseClass :members: send_sms .. autoclass:: flask_security.SmsSenderFactory :members: createSender .. py:class:: OauthCbType[oauth: OAuth, token: t.Any] This callback is called when the oauth redirect happens. It must take the response from the provider and return a tuple of - which will be used to look up the user in the datastore. .. autoclass:: flask_security.OAuthGlue :members: register_provider, register_provider_ext .. autoclass:: flask_security.FsOAuthProvider :members: Extendable Classes ------------------ Each of the following classes can be extended and passed in as part of Security() instantiation. .. autoclass:: flask_security.PhoneUtil :members: :special-members: __init__ .. autoclass:: flask_security.MailUtil :members: :special-members: __init__ .. autoclass:: flask_security.EmailValidateException .. autoclass:: flask_security.PasswordUtil :members: :special-members: __init__ .. autoclass:: flask_security.MfRecoveryCodesUtil :members: :special-members: __init__ .. autoclass:: flask_security.UsernameUtil :members: :special-members: __init__ .. autoclass:: flask_security.WebauthnUtil :members: :special-members: __init__ .. autoclass:: flask_security.Totp :members: get_last_counter, set_last_counter, generate_qrcode Forms ----- .. autoclass:: flask_security.ChangeEmailForm .. autoclass:: flask_security.ChangePasswordForm .. autoclass:: flask_security.ChangeUsernameForm .. autoclass:: flask_security.ConfirmRegisterForm .. autoclass:: flask_security.Form .. autoclass:: flask_security.FormInfo .. autoclass:: flask_security.ForgotPasswordForm .. autoclass:: flask_security.LoginForm .. autoclass:: flask_security.MfRecoveryCodesForm .. autoclass:: flask_security.MfRecoveryForm .. autoclass:: flask_security.PasswordlessLoginForm .. autoclass:: flask_security.RegisterForm .. autoclass:: flask_security.RegisterFormV2 .. autoclass:: flask_security.ResetPasswordForm .. autoclass:: flask_security.SendConfirmationForm .. autoclass:: flask_security.TwoFactorRescueForm .. autoclass:: flask_security.TwoFactorSelectForm .. autoclass:: flask_security.TwoFactorSetupForm .. autoclass:: flask_security.TwoFactorVerifyCodeForm .. autoclass:: flask_security.UnifiedSigninForm .. autoclass:: flask_security.UnifiedSigninSetupForm .. autoclass:: flask_security.UnifiedSigninSetupValidateForm .. autoclass:: flask_security.UnifiedVerifyForm .. autoclass:: flask_security.UsernameRecoveryForm .. autoclass:: flask_security.VerifyForm .. autoclass:: flask_security.WebAuthnDeleteForm .. autoclass:: flask_security.WebAuthnRegisterForm .. autoclass:: flask_security.WebAuthnRegisterResponseForm .. autoclass:: flask_security.WebAuthnSigninForm .. autoclass:: flask_security.WebAuthnSigninResponseForm .. autoclass:: flask_security.WebAuthnVerifyForm .. _signals_topic: Signals ------- See the `Flask documentation on signals`_ for information on how to use these signals in your code. All Flask-Security signals are compatible with Blinker's async implementation. See `Blinker async`_ .. tip:: Remember to add ``**extra_args`` to your signature so that if we add additional parameters in the future your code doesn't break. See the documentation for the signals provided by the Flask-Login and Flask-Principal extensions. In addition to those signals, Flask-Security sends the following signals. .. data:: user_authenticated Sent when a user successfully authenticates. In addition to the app (which is the sender), it is passed `user`, and `authn_via` arguments. The `authn_via` argument specifies how the user authenticated - it will be a list with possible values of ``password``, ``sms``, ``authenticator``, ``email``, ``confirm``, ``reset``, ``register``. .. versionadded:: 3.4.0 .. data:: user_unauthenticated Sent when a user fails to authenticate. It is sent from the `default_unauthn_handler`. It is passed the app (which is the sender). .. versionadded:: 5.4.0 .. data:: user_registered Sent when a user registers on the site. In addition to the app (which is the sender), it is passed `user`, `confirm_token` (deprecated), `confirmation_token` and `form_data` arguments. `form_data` is a dictionary representation of registration form's content received with the registration request. .. data:: user_not_registered Sent when a user attempts to register, but is already registered. This is ONLY sent when :py:data:`SECURITY_RETURN_GENERIC_RESPONSES` is enabled. It is passed the following arguments: * `user` - The existing user model * `existing_email` - True if attempting to register an existing email * `existing_username`- True if attempting to register an existing username * `form_data` - the entire contents of the posted request form .. versionadded:: 5.0.0 .. data:: user_confirmed Sent when a user is confirmed. In addition to the app (which is the sender), it is passed a `user` argument. .. data:: confirm_instructions_sent Sent when a user requests confirmation instructions. In addition to the app (which is the sender), it is passed a `user` and `confirmation_token` arguments. .. data:: login_instructions_sent Sent when passwordless login is used and user logs in. In addition to the app (which is the sender), it is passed `user` and `login_token` arguments. .. data:: password_reset Sent when a user completes a password reset. In addition to the app (which is the sender), it is passed a `user` argument. .. data:: password_changed Sent when a user completes a password change. In addition to the app (which is the sender), it is passed a `user` argument. .. data:: reset_password_instructions_sent Sent when a user requests a password reset. In addition to the app (which is the sender), it is passed `user`, `token` (deprecated), and `reset_token` arguments. .. data:: change_email_instructions_sent Sent when a user requests to change their registered email address. In addition to the app (which is the sender) it is passed `user`, `token`, and `new_email`. .. versionadded:: 5.5.0 .. data:: change_email_confirmed Sent when a user has confirmed their new email address. In addition to the app (which is the sender) it is passed `user`, `old_email`. .. versionadded:: 5.5.0 .. data:: tf_code_confirmed Sent when a user performs two-factor authentication login on the site. In addition to the app (which is the sender), it is passed `user` and `method` arguments. .. versionadded:: 3.3.0 .. data:: tf_profile_changed Sent when two-factor is used and user logs in. In addition to the app (which is the sender), it is passed `user` and `method` arguments. .. versionadded:: 3.3.0 .. data:: tf_disabled Sent when two-factor is disabled. In addition to the app (which is the sender), it is passed `user` argument. .. versionadded:: 3.3.0 .. data:: tf_security_token_sent Sent when a two factor security/access code is sent. In addition to the app (which is the sender), it is passed `user`, `method`, `login_token` and `token` (deprecated) arguments. .. versionadded:: 3.3.0 .. data:: username_changed Sent when a username is successfully changed. In addition to the app (which is the sender), it is passed the `user` and `old_username` arguments. .. data:: username_recovery_email_sent Sent when a username is successfully recovered and sent over email. In addition to the app (which is the sender), it is passed the `user` argument. .. versionadded:: 5.6.0 .. data:: us_security_token_sent Sent when a unified sign in access code is sent. In addition to the app (which is the sender), it is passed `user`, `method`, `token` (deprecated), `login_token`, `phone_number`, and `send_magic_link` arguments. .. versionadded:: 3.4.0 .. data:: us_profile_changed Sent when user completes changing their unified sign in profile. In addition to the app (which is the sender), it is passed `user`, `methods`, and `delete` arguments. `delete` will be set to ``True`` if the user removed a sign in option. .. versionadded:: 3.4.0 .. versionchanged:: 5.0.0 Added delete argument and changed `method` to `methods` which is now a list. .. data:: wan_registered Sent when a WebAuthn credential was successfully created. In addition to the app (which is the sender), it is passed `user` and `name` arguments. .. versionadded:: 5.0.0 .. data:: wan_deleted Sent when a WebAuthn credential was deleted. In addition to the app (which is the sender), it is passed `user` and `name` arguments. .. versionadded:: 5.0.0 .. _Flask async: https://flask.palletsprojects.com/en/3.0.x/async-await/#using-async-and-await .. _Flask documentation on signals: https://flask.palletsprojects.com/en/2.3.x/signals/ .. _Blinker async: https://blinker.readthedocs.io/en/stable/#async-receivers flask-security-5.7.1/docs/authors.rst000066400000000000000000000000301511046741400176340ustar00rootroot00000000000000.. include:: ../AUTHORS flask-security-5.7.1/docs/changelog.rst000066400000000000000000000000341511046741400201020ustar00rootroot00000000000000.. include:: ../CHANGES.rst flask-security-5.7.1/docs/conf.py000066400000000000000000000156051511046741400167320ustar00rootroot00000000000000# # Flask-Security documentation build configuration file, created by # sphinx-quickstart on Mon Mar 12 15:35:21 2012. # # This file is execfile()d with the current directory set to its containing # dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys from pallets_sphinx_themes import ProjectLink from pallets_sphinx_themes import get_version # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) # -- General configuration ----------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "pallets_sphinx_themes", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.autosectionlabel", "sphinx_issues", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "Flask-Security" copyright = "2012-2025" author = "Matt Wright & Chris Wagner" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # release, version = get_version("Flask-Security") # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. # pygments_style = "pocoo" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] nitpicky = True nitpick_ignore = [ ("py:class", "mongoengine.connection"), ("py:class", "ResponseValue"), ("py:class", "AuthenticatorSelectionCriteria"), ("py:class", "UserVerificationRequirement"), ("py:class", "OAuth"), ("py:class", "OAuthError"), ("py:class", "authlib.integrations.flask_client.OAuth"), ("py:class", "TotpMatch"), ("py:class", "t.Type"), ("py:class", "t.Callable"), ("py:class", "t.Any"), ("py:class", "timedelta"), ] autodoc_typehints = "description" # autodoc_mock_imports = ["flask_sqlalchemy"] autodoc_type_aliases = { "CbType": "oauth_provider.CbType", } autosectionlabel_prefix_document = True autosectionlabel_maxdepth = 2 intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "werkzeug": ("https://werkzeug.palletsprojects.com/", None), "flask": ("https://flask.palletsprojects.com/", None), "itsdangerous": ("https://itsdangerous.palletsprojects.com/", None), "sqlalchemy": ("https://docs.sqlalchemy.org/", None), "wtforms": ("https://wtforms.readthedocs.io/", None), "flask_wtforms": ("https://flask-wtf.readthedocs.io", None), "flask_sqlalchemy": ("https://flask-sqlalchemy.palletsprojects.com/", None), "flask_sqlalchemy_lite": ( "https://flask-sqlalchemy-lite.readthedocs.io/en/latest/", None, ), "flask_login": ("https://flask-login.readthedocs.io/en/latest/", None), "passlib": ("https://passlib.readthedocs.io/en/stable", None), "authlib": ("https://docs.authlib.org/en/latest/", None), } # -- Options for HTML output --------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = "flask" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = {"index_sidebar_logo": False} html_context = { "project_links": [ ProjectLink("PyPI releases", "https://pypi.org/project/Flask-Security/"), ProjectLink("Source Code", "https://github.com/pallets-eco/flask-security/"), ProjectLink( "Issue Tracker", "https://github.com/pallets-eco/flask-security/issues/", ), ProjectLink( "Changes", "https://flask-security.readthedocs.io/en/stable/changelog.html", ), ] } # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = "Flask-Security Documentation ({}).format(version)" html_logo = "_static/logo-owl-105.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static", "openapi.yaml"] # Custom sidebar templates, maps document names to template names. html_sidebars = { "index": ["project.html", "localtoc.html", "searchbox.html"], "**": ["relations.html", "searchbox.html", "localtoc.html"], } singlehtml_sidebars = {"index": ["project.html", "localtoc.html"]} # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # Output file base name for HTML help builder. htmlhelp_basename = "Flask-Securitydoc" # -- Options for LaTeX output -------------------------------------------- latex_documents = [ ("index", "Flask-Security.tex", "Flask-Security Documentation", author, "manual") ] # -- Options for sphinx-issues --------------------------------------------- # Github repo issues_github_path = "pallets-eco/flask-security" flask-security-5.7.1/docs/configuration.rst000066400000000000000000002152331511046741400210330ustar00rootroot00000000000000Configuration ============= .. warning:: Be sure to set all configuration (in app.config["xxx"]) *PRIOR* to instantiating the Security class or calling security.init_app(). The following configuration values are used by Flask-Security: Core -------------- These configuration keys are used globally across all features. .. py:data:: SECRET_KEY This is actually part of Flask - but is used by Flask-Security to sign all tokens. It is critical this is set to a strong value. For python3 consider using: ``secrets.token_urlsafe()`` .. py:data:: SECRET_KEY_FALLBACKS This is part of Flask (>=3.1) but can be used by Flask-Security to unsign tokens. See Flask documentation https://flask.palletsprojects.com/en/stable/config/#SECRET_KEY_FALLBACKS .. versionadded:: 5.6.0 .. py:data:: SECURITY_BLUEPRINT_NAME Specifies the name for the Flask-Security blueprint. Default: ``"security"``. .. py:data:: SECURITY_URL_PREFIX Specifies the URL prefix for the Flask-Security blueprint. Default: ``None``. .. py:data:: SECURITY_STATIC_FOLDER Specifies the folder name for static files (webauthn). Default: ``"static"``. .. versionadded:: 5.1.0 .. py:data:: SECURITY_STATIC_FOLDER_URL Specifies the URL for static files used by Flask-Security (webauthn). See Flask documentation https://flask.palletsprojects.com/en/latest/blueprints/#static-files Default: ``"/fs-static"``. .. versionadded:: 5.1.0 .. py:data:: SECURITY_SUBDOMAIN 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 Specifies whether or not to flash messages for actions certain endpoint perform. Normally Flash-Security views will flash informational or error messages only when the operation results in a redirect. Default: ``True``. .. py:data:: SECURITY_I18N_DOMAIN Specifies the name for domain used for translations. Default: ``"flask_security"``. .. py:data:: SECURITY_I18N_DIRNAME Specifies the directory containing the ``MO`` files used for translations. When using flask-babel this can also be a list of directory names - this enables application to override a subset of messages if desired. The default ``builtin`` uses translations shipped with Flask-Security. Default: ``"builtin"``. .. versionchanged:: 5.2.0 "builtin" is a special name which will be interpreted as the ``translations`` directory within the installation of Flask-Security. .. py:data:: SECURITY_TOKEN_AUTHENTICATION_KEY Specifies the query string parameter to read when using token authentication. Default: ``"auth_token"``. .. py:data:: SECURITY_TOKEN_AUTHENTICATION_HEADER Specifies the HTTP header to read when using token authentication. Default: ``"Authentication-Token"``. .. py:data:: SECURITY_TOKEN_MAX_AGE Specifies the number of seconds before an authentication token expires. Default: ``None``, meaning the token never expires. .. py:data:: SECURITY_TOKEN_EXPIRE_TIMESTAMP A callable that returns a unix timestamp in the future when this specific authentication token should expire. Returning 0 means no expiration. It is passed the currently authenticated User so any fields can be used to customize an expiration time. Of course it is called in a request context so any information about the current request can also be used. If BOTH this and :data:`SECURITY_TOKEN_MAX_AGE` are set - the shorter is used. .. note:: These 2 expiry options work differently - with this one, the actual expire timestamp is in the auth_token. The signed token (using itsdangerous) has the timestamp the token was generated. On validation, that is checked against ``SECURITY_TOKEN_MAX_AGE``. So for MAX_AGE, at the time of validation, the token hasn't yet been associated with a User. Default: ``lambda user: 0`` .. py:data:: SECURITY_EMAIL_VALIDATOR_ARGS Email address are validated and normalized via the ``mail_util_cls`` which defaults to :class:`.MailUtil`. That uses the `email_validator`_ package whose methods have configurable options - these can be set here and will be passed in. For example setting this to: ``{"check_deliverability": False}`` is useful when unit testing if the emails are fake. ``mail_util_cls`` has 2 methods - ``normalize`` and ``validate``. Both ensure the passed value is a valid email address, and returns a normalized version. ``validate`` additionally, by default, verifies that the email address can likely actually receive an email. Default: ``None``, meaning use the defaults from email_validator package. .. versionadded:: 4.0.0 .. _email_validator: https://pypi.org/project/email-validator/ .. py:data:: SECURITY_DEFAULT_HTTP_AUTH_REALM Specifies the default authentication realm when using basic HTTP auth. Default: ``Login Required`` .. py:data:: SECURITY_REDIRECT_BEHAVIOR Passwordless login, confirmation, reset password, unified signin, change_email, and oauth signin have GET endpoints that validate the passed token and redirect to an action form. For Single-Page-Applications style UIs which need to control their own internal URL routing these redirects need to not contain forms, but contain relevant information as query parameters. Setting this to ``"spa"`` will enable that behavior. When this is enabled, the following must also be defined: - :py:data:`SECURITY_POST_OAUTH_LOGIN_VIEW` (if :py:data:`SECURITY_OAUTH_ENABLE` is True) - :py:data:`SECURITY_LOGIN_ERROR_VIEW` - :py:data:`SECURITY_CONFIRM_ERROR_VIEW` - :py:data:`SECURITY_POST_CHANGE_EMAIL_VIEW` - :py:data:`SECURITY_CHANGE_EMAIL_ERROR_VIEW` - :py:data:`SECURITY_POST_CONFIRM_VIEW` - :py:data:`SECURITY_RESET_ERROR_VIEW` - :py:data:`SECURITY_RESET_VIEW` Default: ``None`` which is existing html-style form redirects. .. versionadded:: 3.3.0 .. py:data:: SECURITY_REDIRECT_HOST Mostly for development purposes, the UI is often developed separately and is running on a different port than the Flask application. In order to test redirects, the `netloc` of the redirect URL needs to be rewritten. Setting this to e.g. `localhost:8080` does that. .. tip:: Be aware that when this is set, any of the `*_VIEW` configuration variables that are set to URLs and not endpoints, will be redirected to this host. Default: ``None``. .. 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-view redirect targets. This is beneficial if you wish to place your authentication 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_REDIRECT_BASE_DOMAIN Set the base domain for checking allowable redirects. The intent here is to allow an application to be server on e.g. "flaskapp.my.org" and redirect to "myservice.my.org" (which maybe isn't a Flask app). Flask's SERVER_NAME can't be used to verify redirects in this case. Note that in most cases the application will want to set Flask's SESSION_COOKIE_DOMAIN to be this base domain - otherwise authorization information won't be sent. Default: ``None`` .. versionadded:: 5.5.0 .. py:data:: SECURITY_REDIRECT_ALLOWED_SUBDOMAINS A list of subdomains. Each will be prepended to ``SECURITY_REDIRECT_BASE_DOMAIN`` and checked against the requested redirect. Default: ``[]`` .. versionadded:: 5.5.0 .. note:: The above 4 config options apply BOTH to the handling of ``next`` parameter as well as all the ``XXX_VIEW`` URL configuration options for those views that perform a redirect after processing. .. py:data:: SECURITY_CSRF_PROTECT_MECHANISMS Authentication mechanisms that require CSRF protection. These are the same mechanisms as are permitted in the ``@auth_required`` decorator. Default: ``("basic", "session", "token")``. .. py:data:: SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS If ``True`` then CSRF will not be required for endpoints that don't require authentication (e.g. login, logout, register, forgot_password). Default: ``False``. .. py:data:: SECURITY_CSRF_COOKIE_NAME The name for the CSRF cookie. This usually should be dictated by your client-side code - more information can be found at :ref:`csrf_topic` Default: ``None`` - meaning no cookie will be sent. .. py:data:: SECURITY_CSRF_COOKIE A dict that defines the parameters required to set a CSRF cookie. The complete set of parameters is described in Flask's `set_cookie`_ documentation. Default: ``{"samesite": "Strict", "httponly": False, "secure": False}`` .. versionchanged:: 4.1.0 The 'key' attribute was deprecated in favor of a separate configuration variable :data:`SECURITY_CSRF_COOKIE_NAME`. .. py:data:: SECURITY_CSRF_HEADER The HTTP Header name that will contain the CSRF token. ``X-XSRF-Token`` is used by packages such as `axios`_. Default: ``"X-XSRF-Token"``. .. py:data:: SECURITY_CSRF_COOKIE_REFRESH_EACH_REQUEST By default, csrf_tokens have an expiration (controlled by the configuration variable ``WTF_CSRF_TIME_LIMIT``. This can cause CSRF failures if say an application is left idle for a long time. You can set that time limit to ``None`` or have the CSRF cookie sent on every request (which will give it a new expiration time). Default: ``False``. .. py:data:: SECURITY_SCRIPT_NONCE_KEY If set, adds a nonce attribute to all `` {% endblock head_scripts %} {% block content %} {% include "security/_messages.html" %}

{{ _fsdomain("Setup a New Passkey") }}

{% if not credential_options %} {# Initial form to get CreateOptions #}
{{ _fsdomain("Start by providing a unique name for your passkey:") }}
{{ wan_register_form.hidden_tag() }} {{ render_field_with_errors(wan_register_form.name) }} {# Default is just second factor #} {% if config["SECURITY_WAN_ALLOW_AS_FIRST_FACTOR"] %}
{% for subfield in wan_register_form.usage %}{{ render_field_with_errors(subfield) }}{% endfor %}
{% endif %} {{ render_field(wan_register_form.submit) }}
{% else %}
{{ wan_register_response_form.hidden_tag() }}
{% endif %} {% if registered_credentials %}

{{ _fsdomain("Currently registered passkeys:") }}

{% set listing = _fsdomain('Nickname: "%s" Usage: "%s" Transports: "%s" Discoverable: "%s" Device Type: "%s" Backed up? "%s" Last used on: %s') %}
    {% for cred in registered_credentials %}
  • {{ listing|format(cred.name, cred.usage, cred.transports|join(", "), cred.discoverable, cred.device_type, cred.backup_state, cred.lastuse) }}
  • {% endfor %}
{% endif %} {% if wan_delete_form %}

{{ _fsdomain("Delete an Existing Passkey") }}

{# explicitly render csrf_token so we can change the ID so we don't get duplicates #} {{ render_csrf(wan_delete_form, "delete") }} {{ render_field_with_errors(wan_delete_form.name) }} {{ render_field(wan_delete_form.submit) }}
{% endif %} {% if security.support_mfa and security.multi_factor_recovery_codes %}

{{ _fsdomain("Recovery Codes") }}

{{ _fsdomain("This application supports setting up recovery codes.") }} {{ _fsdomain("You can set them up here.") }}
{% endif %} {% include "security/_menu.html" %} {% endblock content %} flask-security-5.7.1/flask_security/templates/security/wan_signin.html000066400000000000000000000053341511046741400264170ustar00rootroot00000000000000{# This template receives the following pieces of context in addition to the form: #} {% set title = title|default(_fsdomain("Sign In With A Passkey")) %} {% extends "security/base.html" %} {% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors, render_form_errors, prop_next, script_nonce %} {% block head_scripts %} {{ super() }} {% endblock head_scripts %} {% block content %} {% include "security/_messages.html" %} {% if not is_secondary %}

{{ _fsdomain("Sign In With a Passkey") }}

{% else %}

{{ _fsdomain("Use a Passkey as a Second Factor") }}

{% endif %} {% if not credential_options %}
{{ wan_signin_form.hidden_tag() }} {{ render_form_errors(wan_signin_form) }} {% if not is_secondary %} {{ render_field_with_errors(wan_signin_form.identity) }} {{ render_field_with_errors(wan_signin_form.remember) }} {% endif %} {{ render_field_errors(wan_signin_form.credential) }} {{ render_field_errors(wan_signin_form.csrf_token) }} {{ render_field(wan_signin_form.submit) }}
{% else %}
{{ wan_signin_response_form.hidden_tag() }} {{ render_field_errors(wan_signin_form.remember) }} {# the following is important even though it is hidden - some browsers require an input focus field (such as Safari) #} {{ render_field(wan_signin_response_form.credential) }}
{% endif %} {% endblock content %} flask-security-5.7.1/flask_security/templates/security/wan_verify.html000066400000000000000000000041211511046741400264250ustar00rootroot00000000000000{# This template receives the following pieces of context in addition to the form: wan_verify_form - wan_signin_response_form - skip_login_menu - True Any other context provided by the "wan_verify" context processer. #} {% set title = title|default(_fsdomain("Reauthenticate")) %} {% extends "security/base.html" %} {% from "security/_macros.html" import render_field_with_errors, render_field, prop_next, script_nonce %} {% block head_scripts %} {{ super() }} {% endblock head_scripts %} {% block content %} {% include "security/_messages.html" %}

{{ _fsdomain("Reauthenticate Using a Passkey") }}

{% if not credential_options %}
{{ wan_verify_form.hidden_tag() }} {{ render_field(wan_verify_form.submit) }}
{% else %}
{{ wan_signin_response_form.hidden_tag() }}
{% endif %} {% endblock content %} flask-security-5.7.1/flask_security/tf_plugin.py000066400000000000000000000310741511046741400220710ustar00rootroot00000000000000""" flask_security.tf_plugin ~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security Two-Factor Plugin Module :copyright: (c) 2022-2024 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. TODO: - add localized callback for select choices. """ from __future__ import annotations import typing as t from flask import request, redirect, session from .decorators import unauth_csrf from .forms import ( build_form_from_request, get_form_field_xlate, Form, RadioField, SubmitField, ) from .proxies import _datastore, _security from .utils import ( _, base_render_json, check_and_get_token_status, config_value as cv, do_flash, get_message, get_within_delta, get_url, login_user, propagate_next, simple_render_json, url_for_security, ) if t.TYPE_CHECKING: # pragma: no cover import flask from flask.typing import ResponseValue from flask import Response from flask_security import Security, UserMixin class TwoFactorSelectForm(Form): which = RadioField(get_form_field_xlate(_("Available Second Factor Methods:"))) submit = SubmitField(get_form_field_xlate(_("Select"))) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @unauth_csrf() def tf_select() -> ResponseValue: # Ask user which MFA method they want to use. # This is used when a user has setup more than one type of 2FA. form = t.cast( TwoFactorSelectForm, build_form_from_request("two_factor_select_form") ) # This endpoint is unauthenticated - make sure we're in a valid state if not all(k in session for k in ["tf_user_id", "tf_select"]): # illegal call on this endpoint tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) user = _datastore.find_user(fs_uniquifier=session["tf_user_id"]) if not user: # pragma no cover # hard to imagine - someone deletes the user while they are logging in. tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) setup_methods = _security.two_factor_plugins.get_setup_tf_methods(user) form.which.choices = setup_methods # type: ignore[assignment] if form.validate_on_submit(): response = None tf_impl = _security.two_factor_plugins.method_to_impl(user, form.which.data) if tf_impl: json_payload = {"tf_required": True} response = tf_impl.tf_login( user, json_payload, next_loc=propagate_next(request.url, None) ) if not response: # pragma no cover # This really can't happen unless between the time the started logging in # and now, they deleted a second factor (which they would have to do # in another window). tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) return response if _security._want_json(request): payload = {"tf_select": True, "tf_setup_methods": [k for k, v in setup_methods]} return base_render_json(form, include_user=False, additional=payload) return _security.render_template( cv("TWO_FACTOR_SELECT_TEMPLATE"), two_factor_select_form=form, **_security._run_ctx_processor("tf_select"), ) class TfPluginBase: # pragma no cover def __init__(self, app: flask.Flask): pass def create_blueprint( self, app: flask.Flask, bp: flask.Blueprint, state: Security ) -> None: raise NotImplementedError def get_setup_methods(self, user: UserMixin) -> list[tuple[str, str]]: """Return a list of tuples representing currently configured methods. The tuple is (value, label) - suitable for use in a FlaskForm Select element. """ raise NotImplementedError def tf_login( self, user: UserMixin, json_payload: dict[str, t.Any], next_loc: str | None ) -> ResponseValue: """ Called from first/primary authenticated views if the user successfully authenticated, and required a second method of authentication. This method returns the necessary information for the user UI to continue. For forms, this is usually a redirect to a secondary sign in form. For JSON it is just a payload that describes what the user has to do next. """ raise NotImplementedError class TfPlugin: """ Two-Factor plugin support. Enables multiple independent two-factor implementations to be configured for a given app. See TfPluginBase for what a new implementation must provide. """ def __init__(self) -> None: self._tf_impls: dict[str, TfPluginBase] = {} def register_tf_impl( # N.B. all methods must be unique across all implementations. self, app: flask.Flask, name: str, impl: t.Type[TfPluginBase], ) -> None: self._tf_impls[name] = impl(app) def create_blueprint( self, app: flask.Flask, bp: flask.Blueprint, state: Security ) -> None: if state.support_mfa: for impl in self._tf_impls.values(): impl.create_blueprint(app, bp, state) # Add our route for selecting between multiple active two-factor # mechanisms. bp.route( cv("TWO_FACTOR_SELECT_URL", app), methods=["GET", "POST"], endpoint="tf_select", )(tf_select) def method_to_impl(self, user: UserMixin, method: str) -> TfPluginBase | None: # reverse map a method to the implementation. # N.B. again - requires that methods be unique across all implementations. # There is a small window that a previously setup method was removed. for impl in self._tf_impls.values(): setup_methods = impl.get_setup_methods(user) if method in [k for k, v in setup_methods]: return impl return None # pragma no cover def get_setup_tf_methods(self, user: UserMixin) -> list[tuple[str, str]]: """Return a list of tuples representing currently configured methods. The tuple is (value, label) - suitable for use in a FlaskForm Select element. """ methods = [] for impl in self._tf_impls.values(): methods.extend(impl.get_setup_methods(user)) return methods def tf_enter( self, user: UserMixin, remember_me: bool | None, primary_authn_via: str, next_loc: str | None, ) -> ResponseValue | None: """Check if two-factor is required and if so, start the process. Must be called in a request context. remember_me controls 2 cookies - the remember_me cookie and the tf_validity cookie. We use the session to hold the fact that the user requested 'remember' across the second factor. """ json_payload: dict[str, t.Any] if _security.support_mfa: tf_setup_methods = [k for k, v in self.get_setup_tf_methods(user)] if cv("TWO_FACTOR_REQUIRED") or len(tf_setup_methods) > 0: tf_fresh = tf_verify_validity_token(user.fs_uniquifier) if cv("TWO_FACTOR_ALWAYS_VALIDATE") or not tf_fresh: # Clean out any potential old session info - in case of previous # aborted 2FA attempt. tf_clean_session() json_payload = {"tf_required": True} if remember_me: session["tf_remember_login"] = remember_me session["tf_user_id"] = user.fs_uniquifier # A backwards compat hack - the original twofactor could be setup # as part of initial login. if len(tf_setup_methods) == 0: # only initial two-factor implementation supports this return self._tf_impls["code"].tf_login( user, json_payload, next_loc ) elif len(tf_setup_methods) == 1: # method_to_impl can't return None here since we just # got the methods up above. impl = t.cast( TfPluginBase, self.method_to_impl(user, tf_setup_methods[0]), ) return impl.tf_login(user, json_payload, next_loc) else: session["tf_select"] = True if not _security._want_json(request): values = dict(next=next_loc) if next_loc else dict() return redirect(url_for_security("tf_select", **values)) # Let's force app to go through tf-select just in case we want # to do further validation... However, provide the choices # so they can just do a POST json_payload.update( { "tf_select": True, "tf_setup_methods": tf_setup_methods, } ) return simple_render_json(json_payload) return None def tf_complete(self, user: UserMixin, dologin: bool) -> str | None: remember = session.pop("tf_remember_login", None) if dologin: login_user(user, remember=remember) tf_clean_session() token = None # return a token to avoid future two-factor prompts (for a period of time) if not cv("TWO_FACTOR_ALWAYS_VALIDATE") and remember: token = generate_tf_validity_token(user.fs_uniquifier) return token def generate_tf_validity_token(fs_uniquifier): """Generates a unique token for the specified user. :param fs_uniquifier: The fs_uniquifier of a user to whom the token belongs to """ return _security.tf_validity_serializer.dumps(fs_uniquifier) 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_validity_token(fs_uniquifier: str) -> bool: """Returns the status of the Two-Factor Validity token based on the current request. :param fs_uniquifier: The ``fs_uniquifier`` of the submitting user. """ token = request.cookies.get("tf_validity", default=None) 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: Response, token: str) -> Response: """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 token: validity token """ cookie_kwargs = cv("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) # This is likely overkill since so far we only return this on a POST which is # unlikely to be cached. response.vary.add("Cookie") return response def tf_check_state(allowed_states: list[str]) -> UserMixin | None: if ( not all(k in session for k in ["tf_user_id", "tf_state"]) or session["tf_state"] not in allowed_states ): tf_clean_session() return None user = _datastore.find_user(fs_uniquifier=session["tf_user_id"]) if not user: tf_clean_session() return user def tf_illegal_state(form, redirect_to): m, c = get_message("TWO_FACTOR_PERMISSION_DENIED") if not _security._want_json(request): do_flash(m, c) return redirect(get_url(redirect_to)) else: form.form_errors.append(m) return base_render_json(form, include_user=False) def tf_clean_session(): """ Clean out ALL stuff stored in session (e.g. on logout or restart of a session) """ if cv("TWO_FACTOR"): for k in [ "tf_state", "tf_user_id", "tf_primary_method", "tf_remember_login", "tf_totp_secret", "tf_select", ]: session.pop(k, None) flask-security-5.7.1/flask_security/totp.py000066400000000000000000000152511511046741400210670ustar00rootroot00000000000000""" flask_security.totp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security TOTP (Timed-One-Time-Passwords) module :copyright: (c) 2019-2024 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from __future__ import annotations import base64 import io import typing as t from passlib.totp import TOTP, TokenError, TotpMatch from passlib.pwd import genword if t.TYPE_CHECKING: # pragma: no cover from flask_security import UserMixin class Totp: """Encapsulate usage of Passlib TOTP functionality. Flask-Security doesn't implement any replay-attack protection out of the box as suggested by: https://passlib.readthedocs.io/en/stable/narr/totp-tutorial.html#match-verify Subclass this and implement the get/set last_counter methods. Your subclass can be registered at Flask-Security creation/initialization time. .. versionadded:: 3.4.0 """ def __init__(self, secrets: dict[str, str] | dict[int, str], issuer: str): """Initialize a totp factory. secrets are used to encrypt the per-user totp_secret on disk. """ # This should be a dict with at least one entry if not isinstance(secrets, dict) or len(secrets) < 1: raise ValueError("secrets needs to be a dict with at least one entry") self._totp = TOTP.using(issuer=issuer, secrets=secrets) def generate_totp_password(self, totp_secret: str) -> str: """Get time-based one-time password on the basis of given secret and time :param totp_secret: the unique shared secret of the user """ tt = self._totp.from_source(totp_secret).generate() return tt.token # type: ignore[return-value] def generate_totp_secret(self) -> str: """Create new user-unique totp_secret. We return an encrypted json string so that when sent in a cookie or sent to DB - it is encrypted. """ return self._totp.new().to_json(encrypt=True) def verify_totp( self, token: str, totp_secret: str, user: UserMixin, window: int = 0 ) -> bool: """Verifies token for specific user. :param token: token to be check against user's secret :param totp_secret: the unique shared secret of the user :param user: User model :param window: optional. How far backward and forward in time to search for a match. Measured in seconds. :return: True if match """ # TODO - in old implementation using onetimepass window was described # as 'compensate for clock skew) and 'interval_length' would say how long # the token is good for. # In passlib - 'window' means how far back and forward to look and 'clock_skew' # is specifically for well, clock slew. try: tmatch = self._totp.verify( token, totp_secret, window=window, last_counter=self.get_last_counter(user), ) self.set_last_counter(user, tmatch) return True except TokenError: return False def get_totp_uri(self, username: str, totp_secret: str) -> str: """Generate provisioning url for use with the qrcode scanner built into the app :param username: username/email of the current user :param totp_secret: a unique shared secret of the user """ tp = self._totp.from_source(totp_secret) return tp.to_uri(username) def get_totp_pretty_key(self, totp_secret: str) -> str: """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: str, user: UserMixin) -> dict[str, str]: """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" assert self._totp.issuer 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: str, totp: str) -> str: """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 qrcode import qrcode.image.svg image = qrcode.make( self.get_totp_uri(username, totp), image_factory=qrcode.image.svg.SvgImage, ) with io.BytesIO() as virtual_file: image.save(virtual_file) 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 generate_recovery_codes(self, number: int) -> list[str]: """Generate a set of secure passwords - used for 2FA recovery codes. # this is nice for english - but not for others return genphrase(entropy="fair", wordset="eff_short", sep="-", returns=number) .. versionadded:: 5.0.0 """ pwds = genword(length=12, charset="hex", returns=number) # make this a bit easier to type - 3 sets of 4 characters spwds = [] for pwd in pwds: spwds.append( "-".join([pwd[i : i + 4] for i in range(0, len(pwd), 4)]) # noqa: E203 ) return spwds def get_last_counter(self, user: UserMixin) -> int | None: """Implement this to fetch stored last_counter from cache. :param user: User model :return: last_counter as stored in set_last_counter() """ return None def set_last_counter(self, user: UserMixin, tmatch: TotpMatch) -> None: """Implement this to cache last_counter. :param user: User model :param tmatch: a TotpMatch as returned from totp.verify() """ pass flask-security-5.7.1/flask_security/translations/000077500000000000000000000000001511046741400222445ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/af_ZA/000077500000000000000000000000001511046741400232245ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/af_ZA/LC_MESSAGES/000077500000000000000000000000001511046741400250115ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/af_ZA/LC_MESSAGES/flask_security.po000066400000000000000000001215651511046741400304120ustar00rootroot00000000000000# Afrikaans (South Africa) translations for Flask-Security. # Copyright (C) 2021 Lonely Viking # This file is distributed under the same license as the Flask-Security # project. # Michael Bosch , 2021. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 4.0.0\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Michael Bosch \n" "Language: af_ZA\n" "Language-Team: af_ZA \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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Intekening Benodig" #: flask_security/core.py:297 msgid "Welcome" msgstr "Welkom" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Bevestig asseblief jou e-pos adres" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Intekening instruksies" #: flask_security/core.py:300 #: 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 "Jou wagwoord is teruggestel" #: flask_security/core.py:301 msgid "Your password has been changed" msgstr "Jou wagwoord is verander" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Wagwoord terugstel instruksies" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "Verifikasie Kode" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "Insette nie geskik vir die versoekte API nie" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "Jy het nie toestemming om hierdie hulpbron te bekyk nie." #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Dankie. Jou e-pos adres is bevestig." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "Jou e-pos adres is reeds bevestig." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "" #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s is reeds geassosieer met 'n rekening." #: flask_security/core.py:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "Identiteits kenmerk '%(attr)s' met waarde '%(value)s' is reeds " "geassosieer met 'n rekening." #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "Wagwoord stem nie ooreen nie" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Wagwoorde stem nie ooreen nie" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Herleiding buite die domein is verbied" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Instruksies om jou wagwoord terug te stel is gestuur na %(email)s." #: flask_security/core.py:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Ongeldige wagwoord terugstellings token." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "E-pos adres benodig bevestiging" #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Bevestigings instruksies is gestuur na %(email)s." #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Jy het nie ingeteken binne %(within)s nie. Nuwe instruksies om in te " "teken is gestuur na %(email)s." #: flask_security/core.py:486 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Instruksies om in te teken is gestuur na %(email)s." #: flask_security/core.py:489 msgid "Invalid login token." msgstr "Ongeldige intekenings token." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Rekening is gestremd." #: flask_security/core.py:491 msgid "Email not provided" msgstr "E-pos is nie voorsien nie" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Ongeldige e-pos adres" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "Ongeldige kode" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Wagwoord is nie voorsien nie" #: flask_security/core.py:496 #, python-format msgid "Password must be at least %(length)s characters" msgstr "Wagwoord moet ten minste %(length)s karakters bevat" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "Wagwoord is te eenvoudig" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "Telefoon nommer is ongeldig, bv. afwesige lands kode" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "Gespesifieerde verbruiker bestaan nie" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Ongeldige wagwoord" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "Voorsiende wagwoord of kode is ongeldig" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Jy het met sukses in geteken" #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Wagwoord vergeet?" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Jy het met sukses jou wagwoord teruggestel en jy het automaties in " "geteken." #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 msgid "Your new password must be different than your previous password." msgstr "Jou nuwe wagwoord moet verskil van jou vorige wagwoord." #: flask_security/core.py:529 msgid "You successfully changed your password." msgstr "Jy het met sukses jou wagwoord verander." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Teken asseblief in om toegang te kry tot hierdie blad." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Herverifieer asseblief om toegang te kry tot hierdie blad." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "Herverifikasie suksesvol" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "Jy kan slegs hierdie eindpunt bereik wanneer jy nie in geteken is nie." #: flask_security/core.py:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "Mislukking met stuur van kode. Probeer asseblief later weer" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "Jy het met sukses jou twee-faktoor metode verander." #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "Jy het tans nie toestemming om hierdie blad te besoek nie" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "Gemerkde metode is nie geldig nie" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "Opstel moet voltooi wees binne %(within)s. Begin asseblief oor." #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "Versoekte metode is nie geldig nie" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "Jy moet 'n valiede identiteit spesifiseer om in te teken" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:586 msgid "Username not provided" msgstr "" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "Stel op deur midde van verifikasie toep (bv. google, lastpass, authy)" #: flask_security/forms.py:64 msgid "Change Method" msgstr "Verander Metode" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Verander Wagwoord" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "Verifikasie Kode" #: flask_security/forms.py:67 msgid "Delete" msgstr "" #: flask_security/forms.py:68 msgid "Email Address" msgstr "E-pos adres" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "Stel up deur midde van e-pos" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "Fout(e)" #: flask_security/forms.py:71 msgid "Identity" msgstr "Identiteit" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Teken In" #: flask_security/forms.py:73 msgid "New Password" msgstr "Nuwe Wagwoord" #: flask_security/forms.py:74 msgid "Passcode" msgstr "Wagkode" #: flask_security/forms.py:75 msgid "Password" msgstr "Wagwoord" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "Telefoon Nommer" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Herstel Wagwoord" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registreer" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Onthou My" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Stel Wagwoord Terug" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Sleutel Weer Wagwoord In" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Stuur Weer Bevestigings Instruksies" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Stuur Intekenings Skakel" #: flask_security/forms.py:85 msgid "Send Code" msgstr "Stuur Kode" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Teken In" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "Stel op deur midde van SMS" #: flask_security/forms.py:88 msgid "Submit" msgstr "Stuur" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "Stuur Kode" #: flask_security/forms.py:90 msgid "Username" msgstr "" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "Bevestig Wagwoord" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "Beskikbare Metodes" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "Kode of Wagwoord" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "Via e-pos" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "Via SMS" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:225 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Bevestig rekening" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Stuur wagwoord herstel instruksies" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Stel wagwoord terug" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Stuur weer bevestigings instruksies" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "Die kode vir verifikasie is gestuur na jou e-pos adres" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Wagwoordlose QRKode" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Kode is gestuur" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Jou wagwoord is verander." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "As jy nie jou wagwoord verander het nie," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "klik hier om dit te herstel" #: 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 "" "As jy nie jou wagwoord verander het nie, klik die skakel hieronder om dit" " te herstel." #: flask_security/templates/security/email/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Welkom %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Jy kan inteken op jou rekening deur die skakel hieronder:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Teken nou in" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Klik hier om jou wagwoord te herstel" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "Klik die skakel hieronder om jou wagwoord te herstel:" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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 "kan nie e-pos rekening bereik nie" #: flask_security/templates/security/email/us_instructions.html:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "Or use the the link below:" #~ msgstr "Of gebruik die skakel hieronder:" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ "Gesamentlik met jou verbruikersnaam en " #~ "wagwoord moet jy 'n kode gebruik " #~ "wat ons vir jou sal stuur" #~ msgid "Please enter your authentication code" #~ msgstr "Sleutel asseblief jou verifikasie kode in" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "Herverifieer asseblief" #~ msgid "Please Enter Your Password" #~ msgstr "Sleutel Asseblief Jou Wagwoord In" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Geen wagwoord is gestel vir hierdie verbruiker nie" #~ msgid "Invalid Token" #~ msgstr "Ongeldige Token" #~ msgid "Your token has been confirmed" #~ msgstr "Jou token is bevestig" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Jy het nie jou wagwoord terug " #~ "gestel binne %(within)s nie. Nuwe " #~ "instruksies is gestuur na %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Jy het nie jou e-pos adres " #~ "bevestig binne %(within)s nie. Nuwe " #~ "instruksies om jou e-pos adres te " #~ "bevestig is gestuur na %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "Jy is nie geverifieer nie. Verskaf asseblief die korrekte getuigskrifte" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ "Om intekening af te handel, sleutel " #~ "asseblief die kode in wat na jou" #~ " e-pos adres gestuur is" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "Na Watter Telefoon Nommer Moet Ons Die Kode Stuur?" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "'n E-pos is gestuur na ons om jou rekening te herstel" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "Dankie. Bevestigings instruksies is gestuur na %(email)s." #~ msgid "Two-factor Login" #~ msgstr "Twee-faktoor Intekening" #~ msgid "Two-factor Rescue" #~ msgstr "Twee-faktoor Redding" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Jy moet herverifieer om toegang te kry tot hierdie eindpunt" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Jy het met sukses twee-faktoor verifikasie afgeskakel" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "Twee-faktoor verifikasie voeg 'n ekstra laag sekuriteit by jou rekening" #~ msgid "Two factor authentication code" #~ msgstr "Twee-faktoor verifikasie kode" #~ msgid "Two-factor Authentication" #~ msgstr "Twee-faktoor Verifikasie" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Verander wagwoord" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Bevestig my rekening" #~ msgid "You can log into your account using the following code:" #~ msgstr "Jy kan inteken op jou rekening deur gebruik van die volgende kode:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "Jy kan inteken op jou rekening deur gebruik van die volgende kode:" #~ msgid "Or use the link below:" #~ msgstr "Of gebruik die skakel hieronder:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Bevestig asseblief jou e-pos adres deur die skakel hieronder:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Jy kan jou e-pos adres bevestig deur die skakel hieronder:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Gebruik hierdie kode om in te teken: %(code)s." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Wagwoord vergeet" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/ar_SA/000077500000000000000000000000001511046741400232315ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/ar_SA/LC_MESSAGES/000077500000000000000000000000001511046741400250165ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/ar_SA/LC_MESSAGES/flask_security.po000066400000000000000000001372771511046741400304260ustar00rootroot00000000000000# Arabic (Saudi Arabia) translations for Flask-Security. # Copyright (C) 2025 CERN # This file is distributed under the same license as the Flask-Security # project. # Sami Alfattany , 2025. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2025-10-30 10:13+0200\n" "Last-Translator: Sami Alfattany \n" "Language: ar_SA\n" "Language-Team: ar_SA \n" "Plural-Forms: nplurals=3; plural=(n > 2);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "تأكيد بريدك الإلكتروني الجديد" #: flask_security/core.py:296 msgid "Login Required" msgstr "تسجيل الدخول مطلوب" #: flask_security/core.py:297 msgid "Welcome" msgstr "مرحبًا" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "يرجى تأكيد بريدك الإلكتروني" #: flask_security/core.py:299 msgid "Login instructions" msgstr "تعليمات تسجيل الدخول" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "تم تغيير كلمة المرور الخاصة بك" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "تعليمات إعادة تعيين كلمة المرور" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "تم تغيير اسم المستخدم الخاص بك" #: flask_security/core.py:304 msgid "Your requested username" msgstr "اسم المستخدم الذي طلبته" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "تسجيل الدخول بخطوتين" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "إنقاذ التحقق بخطوتين" #: flask_security/core.py:350 msgid "Verification Code" msgstr "رمز التحقق" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "المدخلات غير مناسبة لطلب الـAPI" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "فشل التحقق - الهوية أو كلمة المرور/الرمز غير صالح" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "إذا كان عنوان البريد الإلكتروني موجودًا في نظامنا، ستتلقى بريدًا " "إلكترونيًا يشرح كيفية إعادة تعيين كلمة المرور الخاصة بك." #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "إذا كانت الهوية موجودة في نظامنا، فقد تم إرسال رمز إليك." #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "ليس لديك إذن لعرض هذا المورد." #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "يجب عليك تسجيل الدخول لعرض هذا المورد." #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "يجب عليك إعادة التحقق للوصول إلى هذه النقطة النهائية" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" "شكرًا لك. لتأكيد عنوان بريدك الإلكتروني %(email)s، يرجى النقر على الرابط " "في البريد الإلكتروني الذي أرسلناه إليك للتو." #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "شكرًا لك. تم تأكيد بريدك الإلكتروني." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "تم تأكيد بريدك الإلكتروني بالفعل." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "رمز التأكيد غير صالح." #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s مرتبط بالفعل بحساب." #: flask_security/core.py:438 #, 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:444 #, python-format msgid "Identity %(id)s not registered" msgstr "الهوية %(id)s غير مسجلة" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" "حدث خطأ أثناء التواصل مع مزود Oauth: (%(exerror)s - %(exdesc)s). يرجى " "المحاولة مرة أخرى." #: flask_security/core.py:455 msgid "Password does not match" msgstr "كلمة المرور غير متطابقة" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "كلمات المرور غير متطابقة" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "إعادة التوجيه خارج النطاق محظورة" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "رمز الاسترداد غير صالح" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "لم يتم إنشاء رموز استرداد بعد" #: flask_security/core.py:461 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "تم إرسال تعليمات إعادة تعيين كلمة المرور إلى %(email)s." #: flask_security/core.py:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "لم تقم بإعادة تعيين كلمة المرور الخاصة بك خلال %(within)s." #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "رمز إعادة تعيين كلمة المرور غير صالح." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "البريد الإلكتروني يتطلب تأكيدًا." #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "تم إرسال تعليمات التأكيد إلى %(email)s." #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "لم تقم بتأكيد بريدك الإلكتروني خلال %(within)s." #: flask_security/core.py:480 #, 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:486 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "تم إرسال تعليمات تسجيل الدخول إلى %(email)s." #: flask_security/core.py:489 msgid "Invalid login token." msgstr "رمز تسجيل الدخول غير صالح." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "الحساب معطل." #: flask_security/core.py:491 msgid "Email not provided" msgstr "لم يتم تقديم البريد الإلكتروني" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "عنوان البريد الإلكتروني غير صالح" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "رمز غير صالح" #: flask_security/core.py:494 msgid "Password not provided" msgstr "لم يتم تقديم كلمة المرور" #: flask_security/core.py:496 #, python-format msgid "Password must be at least %(length)s characters" msgstr "يجب أن تكون كلمة المرور على الأقل %(length)s أحرف" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "كلمة المرور ليست معقدة بما فيه الكفاية" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "كلمة المرور موجودة في قائمة الاختراقات" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "فشل في الاتصال بموقع كلمات المرور المخترقة" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "رقم الهاتف غير صالح، على سبيل المثال، رمز البلد مفقود" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "المستخدم المحدد غير موجود" #: flask_security/core.py:507 msgid "Invalid password" msgstr "كلمة المرور غير صالحة" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "كلمة المرور أو الرمز المقدم غير صالح" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "لقد قمت بتسجيل الدخول بنجاح." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "هل نسيت كلمة المرور؟" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "لقد قمت بإعادة تعيين كلمة المرور الخاصة بك بنجاح وتم تسجيل دخولك تلقائيًا." #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" "لقد قمت بإعادة تعيين كلمة المرور الخاصة بك بنجاح. يرجى التحقق باستخدام " "كلمة المرور الجديدة الخاصة بك." #: flask_security/core.py:526 msgid "Your new password must be different than your previous password." msgstr "يجب أن تكون كلمة المرور الجديدة مختلفة عن كلمة المرور السابقة." #: flask_security/core.py:529 msgid "You successfully changed your password." msgstr "لقد قمت بتغيير كلمة المرور الخاصة بك بنجاح." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "يرجى تسجيل الدخول للوصول إلى هذه الصفحة." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "يرجى إعادة التحقق للوصول إلى هذه الصفحة." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "تمت إعادة التحقق بنجاح" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "يمكنك الوصول إلى هذه النقطة النهائية فقط عندما لا تكون مسجلاً الدخول." #: flask_security/core.py:537 msgid "Code has been sent." msgstr "تم إرسال الرمز." #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "فشل في إرسال الرمز. يرجى المحاولة مرة أخرى لاحقًا" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "تم تأكيد الرمز الخاص بك" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "لقد قمت بتغيير طريقة التحقق بخطوتين بنجاح." #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "ليس لديك حاليًا أذونات للوصول إلى هذه الصفحة" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "الطريقة المحددة غير صالحة" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "لقد قمت بتعطيل التحقق بخطوتين بنجاح." #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "يجب إكمال الإعداد خلال %(within)s. يرجى البدء من جديد." #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "خيارات تسجيل الدخول النشطة حاليًا: %(method_list)s." #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "الطريقة المطلوبة غير صالحة" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "تم إعداد تسجيل الدخول الموحد بنجاح" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "يجب عليك تحديد هوية صالحة لتسجيل الدخول" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "استخدم هذا الرمز لتسجيل الدخول: %(code)s" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "لقد قمت بتغيير اسم المستخدم الخاص بك بنجاح" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "يجب أن يكون اسم المستخدم على الأقل %(min)d أحرف وأقل من %(max)d أحرف" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "اسم المستخدم يحتوي على أحرف غير قانونية" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "يمكن أن يحتوي اسم المستخدم على الأحرف والأرقام فقط" #: flask_security/core.py:586 msgid "Username not provided" msgstr "لم يتم تقديم اسم المستخدم" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s مرتبط بالفعل بحساب." #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s غير مسجل مع المستخدم الحالي." #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "مقبض مستخدم الاعتماد لم يتطابق" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "يجب إكمال التأكيد خلال %(within)s. يرجى البدء من جديد." #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "تم تأكيد تغيير عنوان البريد الإلكتروني" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "تم إرسال تعليمات لتأكيد عنوان بريدك الإلكتروني الجديد إلى %(email)s." #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "إذا كنت مسجلاً، سيتم إرسال اسم المستخدم الخاص بك إلى بريدك الإلكتروني." #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "إعداد باستخدام تطبيق المصادقة (مثل Google، LastPass، Authy)" #: flask_security/forms.py:64 msgid "Change Method" msgstr "تغيير الطريقة" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "تغيير كلمة المرور" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "رمز المصادقة" #: flask_security/forms.py:67 msgid "Delete" msgstr "حذف" #: flask_security/forms.py:68 msgid "Email Address" msgstr "عنوان البريد الإلكتروني" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "إعداد باستخدام البريد الإلكتروني" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "خطأ/أخطاء" #: flask_security/forms.py:71 msgid "Identity" msgstr "الهوية" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "تسجيل الدخول" #: flask_security/forms.py:73 msgid "New Password" msgstr "كلمة المرور الجديدة" #: flask_security/forms.py:74 msgid "Passcode" msgstr "رمز المرور" #: flask_security/forms.py:75 msgid "Password" msgstr "كلمة المرور" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "رقم الهاتف" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "استعادة كلمة المرور" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "استعادة اسم المستخدم" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "تسجيل" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "تذكرني" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "إعادة تعيين كلمة المرور" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "أعد كتابة كلمة المرور" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "إعادة إرسال تعليمات التأكيد" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "إرسال رابط تسجيل الدخول" #: flask_security/forms.py:85 msgid "Send Code" msgstr "إرسال الرمز" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "تسجيل الدخول" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "إعداد باستخدام الرسائل النصية" #: flask_security/forms.py:88 msgid "Submit" msgstr "إرسال" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "إرسال الرمز" #: flask_security/forms.py:90 msgid "Username" msgstr "اسم المستخدم" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "التحقق من كلمة المرور" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "Google Authenticator" #: flask_security/forms.py:97 msgid "authenticator" msgstr "المصادقة" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "البريد الإلكتروني" #: flask_security/forms.py:100 msgid "SMS" msgstr "الرسائل النصية" #: flask_security/forms.py:101 msgid "password" msgstr "كلمة المرور" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "لا شيء" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "الطرق المتاحة" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "تعطيل المصادقة بخطوتين" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "هل تواجه مشكلة في الوصول إلى حسابك؟/هل فقدت جهازك المحمول؟" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "اتصل بالمسؤول" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "عرض رموز الاسترداد" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "إنشاء رموز استرداد جديدة" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "رمز الاسترداد" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "طرق العامل الثاني المتاحة:" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "اختر" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "إرسال الرمز عبر البريد الإلكتروني" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "استخدام رمز الاسترداد الذي تم تنزيله مسبقًا" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "الرمز أو كلمة المرور" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "عبر البريد الإلكتروني" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "عبر الرسائل النصية" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "إعداد خيار تسجيل دخول إضافي" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "حذف خيار تسجيل الدخول النشط" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "الاسم المستعار" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "الاستخدام" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "استخدام كعامل مصادقة أول" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "استخدام كعامل مصادقة ثانوي" #: flask_security/webauthn.py:225 msgid "Start" msgstr "ابدأ" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "قائمة" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "خروج" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "تغيير البريد المسجل" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "تغيير اسم المستخدم" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "إعداد التحقق بخطوتين" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "إعداد تسجيل الدخول الموحد" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "تسجيل الدخول الموحد" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "تأكيد الحساب" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "تغيير البريد الإلكتروني" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" "بمجرد الإرسال، سيتم إرسال رسالة تأكيد إلى عنوان البريد الإلكتروني الجديد " "هذا." #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "ليس لديك كلمة مرور حالياً - سيتم إضافة واحدة." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "اسم المستخدم الحالي هو: %(username)s" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "إرسال تعليمات إعادة تعيين كلمة المرور" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "أو" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "استخدم Oauth الاجتماعي لتسجيل الدخول" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "تسجيل الدخول باستخدام %(provider)s" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "أدخل رمز الاسترداد" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "رموز الاسترداد" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "تأكد من نسخ هذه الرموز وحفظها في مكان آمن. يمكن استخدام كل رمز مرة واحدة " "فقط." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "إنشاء رموز استرداد جديدة" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "استعادة اسم المستخدم" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "إعادة تعيين كلمة المرور" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "إعادة إرسال تعليمات التأكيد" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "اختر طريقة التحقق بخطوتين" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "المصادقة بخطوتين تضيف طبقة أمان إضافية إلى حسابك" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "بالإضافة إلى اسم المستخدم وكلمة المرور، ستحتاج إلى استخدام رمز." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "طريقة التحقق بخطوتين الحالية: %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "رمز المصادقة بخطوتين" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "أدخل الرمز لإكمال الإعداد" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "أدخل رمزًا رقميًا" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "هذا التطبيق يدعم إعداد رموز الاسترداد." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "يمكنك إعدادها هنا." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "المصادقة بخطوتين" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "يرجى إدخال رمز المصادقة الذي تم إنشاؤه عبر: %(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "تم إرسال رمز المصادقة إلى بريدك الإلكتروني" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "تم إرسال بريد إلكتروني إلينا لإعادة تعيين حساب التطبيق الخاص بك" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "إعداد تسجيل الدخول الموحد" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "رمز الاستجابة السريعة بدون كلمة مرور" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "لم يتم تفعيل أي طرق - لا يوجد شيء لإعداده" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "أدخل الرمز هنا لإكمال الإعداد" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "طلب إرسال رمز لمرة واحدة" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "إعادة التحقق" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "تم إرسال الرمز" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "الاسم المستعار: \"%s\" الاستخدام: \"%s\" وسائل النقل: \"%s\" قابل " "للاكتشاف: \"%s\" نوع الجهاز: \"%s\" تم النسخ الاحتياطي؟ \"%s\" آخر " "استخدام في: %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" "استخدم هذا الرابط لتأكيد عنوان بريدك الإلكتروني " "الجديد." #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "سينتهي صلاحية هذا الرابط خلال %(within)s." #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "البريد الإلكتروني المسجل حالياً هو %(email)s." #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "استخدم %(link)s لتأكيد عنوان بريدك الإلكتروني الجديد." #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "تم تغيير كلمة المرور الخاصة بك." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "إذا لم تقم بتغيير كلمة المرور الخاصة بك،" #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "تم تغيير اسم المستخدم الخاص بك." #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" "استخدم هذا الرابط لتأكيد عنوان " "بريدك الإلكتروني." #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "استخدم %(confirmation_link)s لتأكيد عنوان بريدك الإلكتروني." #: 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "مرحباً %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:4 msgid "Login now" msgstr "سجّل الدخول الآن" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "انقر هنا لإعادة تعيين كلمة المرور الخاصة بك" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "انقر على الرابط أدناه لإعادة تعيين كلمة المرور الخاصة بك:" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "مرحباً %(username)s!" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "يمكنك تسجيل الدخول إلى حسابك باستخدام الرمز التالي: %(token)s" #: 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:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "يمكنك تسجيل الدخول إلى حسابك باستخدام الرمز التالي: %(token)s" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "أو استخدم هذا الرابط: تسجيل الدخول" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "يمكنك تسجيل الدخول إلى حسابك باستخدام الرمز التالي: %(token)s." #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "أو استخدم هذا الرابط: %(login_link)s" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "مرحباً،" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "لقد طلبت مؤخرًا استعادة اسم المستخدم الخاص بك." #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "اسم المستخدم الخاص بك هو: %(username)s" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "إذا لم تقم بطلب ذلك، يمكنك تجاهل هذا البريد الإلكتروني بأمان." #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "مرحباً %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "شخص ما (أنت؟) حاول تسجيل هذا البريد الإلكتروني - وهو موجود بالفعل في " "نظامنا." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "هذا الحساب مرتبط أيضاً باسم المستخدم التالي: %(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" "يمكنك استخدام هذا الرابط لإعادة تعيين كلمة" " المرور الخاصة بك." #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" "لم تقم بتأكيد عنوان بريدك الإلكتروني بعد - استخدم هذا الرابط للقيام بذلك الآن." #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "هذا الحساب مرتبط أيضاً باسم المستخدم التالي: %(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" "يمكنك استخدام هذا الرابط %(reset_link)s لإعادة تعيين كلمة المرور الخاصة " "بك." #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" "لم تقم بتأكيد عنوان بريدك الإلكتروني بعد - استخدم هذا الرابط: " "%(confirmation_link)s للقيام بذلك الآن." #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "حاولت التسجيل باسم المستخدم \"%(username)s\" وهو مرتبط بالفعل بحساب آخر." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "يرجى إعادة بدء عملية التسجيل باسم مستخدم مختلف." #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "يجب إكمال عملية WebAuthn خلال %(within)s. يرجى البدء من جديد." #~ msgid "Nickname for new credential is required." #~ msgstr "الاسم المستعار للاعتماد الجديد مطلوب." #~ msgid "%(name)s is already associated with a credential." #~ msgstr "%(name)s مرتبط بالفعل باعتماد." #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "تم حذف اعتماد WebAuthn بنجاح بالاسم: %(name)s" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "تمت إضافة اعتماد WebAuthn بنجاح بالاسم: %(name)s" #~ msgid "WebAuthn credential id already registered." #~ msgstr "معرف اعتماد WebAuthn مسجل بالفعل." #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "معرف اعتماد WebAuthn غير مسجل." #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "اعتماد WebAuthn لا ينتمي إلى أي مستخدم." #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "تعذر التحقق من اعتماد WebAuthn: %(cause)s." #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "الاعتماد غير مسجل لهذا الاستخدام (الأولي أو الثانوي)" #~ msgid "webauthn" #~ msgstr "WebAuthn" #~ msgid "WebAuthn Setup" #~ msgstr "إعداد WebAuthn" #~ msgid "Forgot password" #~ msgstr "هل نسيت كلمة المرور" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "استخدم WebAuthn لتسجيل الدخول" #~ msgid "Sign in with WebAuthn" #~ msgstr "تسجيل الدخول باستخدام WebAuthn" #~ msgid "WebAuthn" #~ msgstr "WebAuthn" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "هذا التطبيق يدعم مفاتيح أمان WebAuthn." #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "استخدم مفتاح أمان WebAuthn لإعادة التحقق" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "إعداد مفتاح أمان WebAuthn جديد" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "ابدأ بتقديم اسم فريد لمفتاح الأمان الجديد الخاص بك:" #~ msgid "Currently registered security keys:" #~ msgstr "مفاتيح الأمان المسجلة حالياً:" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "حذف مفتاح أمان WebAuthn الحالي" #~ msgid "WebAuthn Security Key" #~ msgstr "مفتاح أمان WebAuthn" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "تسجيل الدخول باستخدام مفتاح أمان WebAuthn" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "استخدم مفتاح أمان WebAuthn الخاص بك كعامل ثانٍ" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "إعادة التحقق باستخدام مفتاح أمان WebAuthn الخاص بك" flask-security-5.7.1/flask_security/translations/ca_ES/000077500000000000000000000000001511046741400232165ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/ca_ES/LC_MESSAGES/000077500000000000000000000000001511046741400250035ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.po000066400000000000000000001164671511046741400304110ustar00rootroot00000000000000# Catalan (Spain) translations for Flask-Security. # Copyright (C) 2017 DINSIC # This file is distributed under the same license as the Flask-Security # project. # Orestes Sanchez , 2018. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 3.1.0\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2019-06-16 00:12+0200\n" "Last-Translator: Orestes Sanchez \n" "Language: ca_ES\n" "Language-Team: \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Per poder veure la pàgina sol·licitada és necessari iniciar la sessió" #: flask_security/core.py:297 msgid "Welcome" msgstr "Benvingut" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Si us plau, confirmeu el vostre correu electrònic" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Instruccions d'inici de la sessió" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "S'ha canviat la teva contrasenya" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Instruccions de recuperació de la contrasenya" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 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:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Moltes gràcies. S'ha confirmat el teu correu electrònic." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "El teu correu electrònic ja s'havia confirmat." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Token de confirmació no vàlid." #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s ja es associat amb un compte." #: flask_security/core.py:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "La contrasenya no coincideix" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Les contrasenyes no coincideixen" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Les redireccions a llocs web externes s'han prohibit" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, 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:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "El token per restablir la contrasenya no és vàlid." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "El correu electrònic requereix d'una confirmació." #: flask_security/core.py:471 #, 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:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "No vas iniciar la sessió abans de %(within)s. S'han enviat noves " "instruccions a %(email)s." #: flask_security/core.py:486 #, 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:489 msgid "Invalid login token." msgstr "Token de d'inici de sessió no vàlid." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "el compte està desactivat." #: flask_security/core.py:491 msgid "Email not provided" msgstr "No s'ha inclòs el correu electrònic" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Adreça de correu electrònic no vàlida" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "" #: flask_security/core.py:494 msgid "Password not provided" msgstr "No s'ha inclòs la contrasenya" #: flask_security/core.py:496 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" msgstr "La contrasenya ha de tenir al menys %(length)s caràcters" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "L'usuari no existeix" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Contrasenya no vàlida" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "La sessió s'ha iniciat amb èxit." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Has oblidat la teva contrasenya?" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Has restablert la teva contrasenya amb èxit i s'ha iniciat la sessió " "automàticament." #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 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:529 msgid "You successfully changed your password." msgstr "La teva contrasenya s'ha modificat amb èxit." #: flask_security/core.py:530 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:531 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:532 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:586 msgid "Username not provided" msgstr "" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:64 msgid "Change Method" msgstr "" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Canvi de contrasenya" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:67 msgid "Delete" msgstr "" #: flask_security/forms.py:68 msgid "Email Address" msgstr "Correu electrònic" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "" #: flask_security/forms.py:71 msgid "Identity" msgstr "" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Iniciar sessió" #: flask_security/forms.py:73 msgid "New Password" msgstr "Nova contrasenya" #: flask_security/forms.py:74 msgid "Passcode" msgstr "" #: flask_security/forms.py:75 msgid "Password" msgstr "Contrasenya" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Restablir la contrasenya" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registrar-se" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Recorda'm" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Restablir la contrasenya" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Escriu la contrasenya una altra vegada" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Reenviar les instruccions de confirmació" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Enviar l'enllaç d'inici de sessió" #: flask_security/forms.py:85 msgid "Send Code" msgstr "" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:88 msgid "Submit" msgstr "" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "" #: flask_security/forms.py:90 msgid "Username" msgstr "" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:225 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menú" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Confirmació de compte" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Enviar instruccions per restablir la contrasenya" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Restablir la contrasenya" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Reenviar instruccions de confirmació" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "S'ha canviat la teva contrasenya." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Si no has canviat la teva contrasenya," #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Benvingut %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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í:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Iniciar sessió ara" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Feu clic aquí per restablir la contrasenya" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." 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 "" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ msgid "Please enter your authentication code" #~ msgstr "" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "" #~ msgid "Please Enter Your Password" #~ msgstr "" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "No hi ha cap contrasenya per a l'usuari" #~ msgid "Invalid Token" #~ msgstr "" #~ msgid "Your token has been confirmed" #~ msgstr "" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "No vas restablir la teva contrasenya " #~ "abans de %(within)s. S'han enviat noves" #~ " instruccions a %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "No vas confirmar el teu correu " #~ "electrònic abans de %(within)s. S'han " #~ "enviat noves instruccions a %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ 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." #~ msgid "Two-factor Login" #~ msgstr "" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Canviar la contrasenya" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Confirmeu el compte" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Confirma el teu correu electrònic fent clic aquí:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Confirmeu el vostre correu electrònic fent clic a continuació:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Contrasenya oblidada" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/da_DK/000077500000000000000000000000001511046741400232065ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/da_DK/LC_MESSAGES/000077500000000000000000000000001511046741400247735ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/da_DK/LC_MESSAGES/flask_security.po000066400000000000000000001154661511046741400303770ustar00rootroot00000000000000# Danish (Denmark) translations for Flask-Security. # Copyright (C) 2017 ORGANIZATION # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2017. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.1.0\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2017-03-23 14:04+0100\n" "Last-Translator: Leonhard Printz \n" "Language: da_DK\n" "Language-Team: da_DK \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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Login påkræveet" #: flask_security/core.py:297 msgid "Welcome" msgstr "Velkommen" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Bekræft venligst din email" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Logininstruktioner" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "Din adgangskode er blevet ændret" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Instruktioner til nulstilling af adganskode" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "Du har ikke adgang til denne resource." #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Mange Tak. Din email er blevet bekræftet." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "Din email er allerede blevet bekræftet." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Ugyldig bekræftigelsestoken." #: flask_security/core.py:433 #, 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:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "Adgangskode passer ikke" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Adgangskoderne passer ikke" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Omdirigering udenfor domænet er forbudt" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, 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:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Ugyldig nulstillingstoken." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "Email kræver bekræftigelse." #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Bekræftigelsesinstruktioner er blevet sendt til %(email)s." #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Du har ikke logget in indenfor %(within)s. Nye logininstruktioner er " "blevet sendt til %(email)s." #: flask_security/core.py:486 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Logininstruktioner er blevet sendt til %(email)s." #: flask_security/core.py:489 msgid "Invalid login token." msgstr "Ugyldig logintoken." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Kontoen er deaktiveret." #: flask_security/core.py:491 msgid "Email not provided" msgstr "Email ikke angivet" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Ugyldig email adresse" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Adgangskode ikke angivet" #: flask_security/core.py:496 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" msgstr "Adgangskoden skal indeholde mindst %(length)s tegn" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "Denne bruger findes ikke" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Ugyldig adgangskode" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Du er hermed blevet logget ind." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Glemt adgangskode?" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Du har hermed nulstillet din adgangskode og er blevet automatisk logget " "ind." #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 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:529 msgid "You successfully changed your password." msgstr "Du har hermed ændret din adgangskode." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Log in for at få adgang til denne side." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Bekræft identitet for at få adgang til denne side." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:586 msgid "Username not provided" msgstr "" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:64 msgid "Change Method" msgstr "" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Ændre adgangskode" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:67 msgid "Delete" msgstr "" #: flask_security/forms.py:68 msgid "Email Address" msgstr "Email adresse" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "" #: flask_security/forms.py:71 msgid "Identity" msgstr "" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Login" #: flask_security/forms.py:73 msgid "New Password" msgstr "Ny adgangskode" #: flask_security/forms.py:74 msgid "Passcode" msgstr "" #: flask_security/forms.py:75 msgid "Password" msgstr "Adgangskode" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Genopret adgangskode" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registrer" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Husk" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Nulstil adgangskode" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Gentast adgangskode" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Gensend bekræftelsesinstruktioner" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Send login link" #: flask_security/forms.py:85 msgid "Send Code" msgstr "" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:88 msgid "Submit" msgstr "" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "" #: flask_security/forms.py:90 msgid "Username" msgstr "" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:225 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Bekræft konto" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Send adgangskode nulstillingsinstruktioner" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Nulstil adgangskode" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Gensend bekræftelsesinstruktioner" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Din adgangskode er blevet ændret." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Hvis du ikke har ændret din adgangskode," #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Velkommen %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Login" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Klik her for at nulstille din adgangskode" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." 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 "" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ msgid "Please enter your authentication code" #~ msgstr "" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "" #~ msgid "Please Enter Your Password" #~ msgstr "" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Denne bruger har ingen adganskode" #~ msgid "Invalid Token" #~ msgstr "" #~ msgid "Your token has been confirmed" #~ msgstr "" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Du har ikke nulstillet din adgangskode" #~ " indenfor %(within)s. Nye instruktioner er" #~ " sendt til %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Du har ikke bekræftet din email " #~ "indenfor %(within)s. Nye instruktioner er " #~ "blevet sendt til %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "Mange tak. Bekræftelsesinstruktioner er blevet sendt til %(email)s." #~ msgid "Two-factor Login" #~ msgstr "" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Ændre adgangskode" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Bekræft ny konto" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Bekræft venligst din email gennem nedenstående link:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Bekræft venligst din email gennem nedenstående link:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Glemt din adgangskode" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/de_DE/000077500000000000000000000000001511046741400232045ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/de_DE/LC_MESSAGES/000077500000000000000000000000001511046741400247715ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/de_DE/LC_MESSAGES/flask_security.po000066400000000000000000001366611511046741400303750ustar00rootroot00000000000000# German translation for Flask-Security # Copyright (C) 2017-2021 ORGANIZATION # This file is distributed under the same license as the Flask-Security # project. # Ingo Kleiber , 2017, # Erich Seifert , 2017. # Pascua Theus , 2021-2022 # msgid "" msgstr "" "Project-Id-Version: Flask-Security 4.1.3\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2022-04-05 13:50+0200\n" "Last-Translator: Pascua Theus \n" "Language: de_DE\n" "Language-Team: de_DE \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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Anmeldung erforderlich" #: flask_security/core.py:297 msgid "Welcome" msgstr "Willkommen" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Bitte E-Mail-Adresse bestätigen" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Anmeldeanleitung" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "Das Passwort wurde geändert" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Anleitung zur Passwortwiederherstellung" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "Verifizierungscode" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "Ungültige Eingabe für die angeforderte Ressource" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" "Authentifizierung fehlgeschlagen – Identität oder Passwort/Passcode " "ungültig" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Wenn diese E-Mail-Adresse bei uns existiert, erhalten Sie eine E-Mail, in" " der beschrieben wird, wie Sie Ihr Passwort zurücksetzen können." #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "Wenn diese Identität bei uns existiert, wird Ihnen ein Code zugesandt." #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "Sie haben keine Berechtigung, um diese Ressource zu sehen." #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" "Vielen Dank. Um Ihre E-Mail-Adresse %(email)s zu bestätigen, klicken Sie " "bitte auf den Link in der E-Mail, die wir gerade an Sie gesendet haben." #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Vielen Dank. Die E-Mail-Adresse wurde bestätigt." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "Die E-Mail-Adresse wurde bereits bestätigt." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Ungültiger Bestätigungscode." #: flask_security/core.py:433 #, 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:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "Benutzermerkmal '%(attr)s' mit Wert '%(value)s' ist bereits mit einem " "anderen Benutzerkonto verknüpft" #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "Benutzer %(id)s nicht registriert" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" "Es trat ein Fehler bei der Kommunikation mit dem OAuth-Provider auf. " "Bitte versuchen Sie es erneut." #: flask_security/core.py:455 msgid "Password does not match" msgstr "Das Passwort stimmt nicht überein" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Die Passwörter stimmen nicht überein" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Weiterleitungen außerhalb der Domain sind verboten" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "Wiederherstellungscode ungültig" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "Es wurden noch keine Sicherheitscodes generiert." #: flask_security/core.py:461 #, 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:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "Sie haben ihr Passwort nicht innerhalb von %(within)s zurückgesetzt. " #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Ungültiger Passwortwiederherstellungscode." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "Die E-Mail-Adresse muss bestätigt werden." #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "" "Um Ihre E-Mail-Adresse %(email)s zu bestätigen, klicken Sie bitte auf den" " Link in der E-Mail, die wir gerade an Sie gesendet haben." #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "Sie haben Ihre E-Mail-Adresse nich innerhalb von $(within)s bestätigt." #: flask_security/core.py:480 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Die Anmeldung erfolgte nicht in %(within)s. Eine neue Anleitung wurde an " "%(email)s gesendet." #: flask_security/core.py:486 #, 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:489 msgid "Invalid login token." msgstr "Ungültiger Anmeldecode." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Konto ist deaktiviert." #: flask_security/core.py:491 msgid "Email not provided" msgstr "Keine E-Mail-Adresse angegeben" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Ungültige E-Mail-Adresse" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "Ungültiger Code" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Kein Passwort angegeben" #: flask_security/core.py:496 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" msgstr "Das Passwort muss mindestens %(length)s Zeichen lang sein" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "Passwort ist nicht komplex genug" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "Passwort ist öffentlich bekannt" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "Konnte keine Verbindung zum Dienst aufbauen, um Passwörter zu überprüfen." #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "Telefonnumer ist ungültig, eventuell fehlt die Landesvorwahl" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "Der angegebene Benutzer existiert nicht" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Ungültiges Passwort" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "Übermitteltes Passwort oder Code ist ungültig" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Sie wurden angemeldet." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Passwort vergessen?" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Das Passwort wurde erfolgreich wiederhergestellt und die Anmeldung " "erfolgte automatisch." #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" "Sie haben Ihr Passwort erfolgreich zurückgesetzt. Bitte bestätigen Sie " "Ihr neues Passwort." #: flask_security/core.py:526 msgid "Your new password must be different than your previous password." msgstr "Das neue Passwort muss sich vom vorherigen unterscheiden." #: flask_security/core.py:529 msgid "You successfully changed your password." msgstr "Das Passwort wurde erfolgreich geändert." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Bitte melden Sie sich an, um diese Seite zu sehen." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Bitte neu anmelden, um auf diese Seite zuzugreifen." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "Neuanmeldung erfolgreich" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "Dieser Endpunkt ist nur für angemeldete Nutzer erlaubt." #: flask_security/core.py:537 msgid "Code has been sent." msgstr "Code wurde gesendet." #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "" "Zusendung des Codes fehlgeschlagen. Bitte versuchen Sie es später noch " "einmal." #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "Ihr Code wurde bestätigt." #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "Sie haben Ihre Zwei-Faktor-Methode erfolgreich geändert." #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "Sie haben aktuell nicht die nötigen Rechte, um die Seite anzusehen" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "Ausgewählte Methode ist nicht gültig" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" "Einrichtung muss innerhalb %(within)s abgeschlossen werden. Bitte neu " "beginnen." #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "Angefragte Methode ist ungültig" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "Single-User-Login erfolgreich eingerichtet" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "Sie müssen eine gültige Identität auswählen, um sich anzumelden" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "Der Benutzername muss mindestens %(min)d und darf nicht länger als " "%(max)d Zeichen sein." #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "Der Benutzername enthält ungültige Zeichen." #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "Der Benutzername darf nur aus Buchstaben und Ziffern bestehen" #: flask_security/core.py:586 msgid "Username not provided" msgstr "Benutzername nicht angegeben" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s ist bereits mit einem Benutzerkonto verknüpft." #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s ist für den aktuellen Benutzer nicht eingetragen." #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "Benutzerhandle des WebAuthn-Token stimmt nicht überein." #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "Einrichtung via Authentisierungs-App" #: flask_security/forms.py:64 msgid "Change Method" msgstr "Methode ändern" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Passwort ändern" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "Authentisierungscode" #: flask_security/forms.py:67 msgid "Delete" msgstr "Löschen" #: flask_security/forms.py:68 msgid "Email Address" msgstr "E-Mail-Adresse" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "Einrichtung via E-Mail" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "Fehler" #: flask_security/forms.py:71 msgid "Identity" msgstr "Identität" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Anmelden" #: flask_security/forms.py:73 msgid "New Password" msgstr "Neues Passwort" #: flask_security/forms.py:74 msgid "Passcode" msgstr "Anmelde-Code" #: flask_security/forms.py:75 msgid "Password" msgstr "Passwort" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "Telefonnummer" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Passwort wiederherstellen" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registrieren" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Erinnern" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Passwort zurücksetzen" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Passwort erneut eingeben" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Bestätigungslink erneut senden" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Anmelde-Link versenden" #: flask_security/forms.py:85 msgid "Send Code" msgstr "Sende Code" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Anmeldung" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "Einrichtung via SMS" #: flask_security/forms.py:88 msgid "Submit" msgstr "Bestätigen" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "Code bestätigen" #: flask_security/forms.py:90 msgid "Username" msgstr "Benutzername" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "Password bestätigen" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "Verfügbare Methoden" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "Probleme beim Zugriff auf Ihr Konto / Smartphone verloren?" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "Kontaktiere einen Administrator" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "Wiederherstellungscode anzeigen" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "Neue Wiederherstellungscodes erzeugen" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "Wiederherstellungscode" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Verfügbare Zwei-Faktor-Methoden:" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "Auswählen" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "Sende code per E-Mail" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "Zuvor heruntergeladenen Wiederherstellungscode verwenden" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "Code oder Passwort" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "Via E-Mail" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "Via SMS" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "Richte weitere Single-User-Login-Option ein" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "Löschen der aktivierten Anmeldeoption" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "Nickname" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "Benutzung" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "Als ersten Faktor benutzen" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "Als zweiten Faktor benutzen" #: flask_security/webauthn.py:225 msgid "Start" msgstr "Start" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menü" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "Abmelden" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "Single-User-Login-Einrichtung" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "Single-User-Login" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Konto bestätigen" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "Sie haben noch kein Passwort – hier können Sie eines setzen." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Anleitung zur Passwortzurücksetzung versenden" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "oder" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "Nutze Social OAuth zur Anmeldung" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Wiederherstellungscode eingeben" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Wiederherstellungscodes" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Kopieren Sie diese unbedingt und bewahren Sie die Codes an einem sicheren" " Ort auf. Jeder Code kann nur einmal verwendet werden." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "Erzeuge neue Wiederherstellungscodes" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Passwort zurücksetzen" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Bestätigungslink erneut versenden" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "Sie müssen zusätzlich zu Benutzername und Passwort einen Code angeben" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Aktivierte Zwei-Faktor-Methode: %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Öffnen Sie die Authentisierungs-App z.B. auf Ihrem Smartphone und scannen" " Sie den folgenden QR-Code (oder geben Sie den unten stehenden Code ein)," " um den Empfang von Codes zu beginnen" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Diese Anwendung unterstützt die Einrichtung von Wiederherstellungscodes." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Sie können diese hier einrichten." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" "Bitte geben Sie den Authentisierungscode ein, den Sie via %(method)s " "erhalten haben." #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "Der Authentisierungscode wurde an Ihre E-Mail-Adresse gesendet" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Single-User-Login einrichten" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Passwortloser QR-Code" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Keine Methode ausgewählt – keine Einrichtung erfolgt" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "Geben Sie den Code hier ein, um die Einrichtung abzuschließen" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Senden eines einmaligen Codes anfordern" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Der Code wurde verschickt" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Ihr Passwort wurde geändert." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Falls Sie Ihr Passwort nicht geändert haben," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "klicken Sie hier, 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 "" "Wenn Sie Ihr Passwort nicht geändert haben, klicken Sie bitte auf den " "unten stehenden Link." #: flask_security/templates/security/email/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Willkommen %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Jetzt anmelden" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Klicken Sie hier, um das Passwort zurückzusetzen" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "Klicken Sie auf den unten stehenden Link, um Ihr Passwort zurückzusetzen:" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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 "kann E-Mail-Konto nicht erreichen" #: flask_security/templates/security/email/us_instructions.html:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "Hallo %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Jemand, eventuell Sie selbst, hat versucht sich mit dieser E-Mail, die " "bereits in unserem System ist, zu registrieren." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "Dieses Konto hat auch den folgenden Benutzernamen: %(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "Dieses Konto hat auch den folgenden Benutzernamen: %(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Sie haben versucht, sich mit dem Benutzernamen \"%(username)s\" " "anzumelden. Dieser Benutzername ist bereits vergeben." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" "Bitte starten Sie den Registrierungsprozess mit einem anderen " "Benutzernamen erneut." #~ 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 "" #~ msgid "Or use the the link below:" #~ msgstr "Oder nutzen Sie den folgenden Link:" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ "Zusätzlich zu Ihrem Benutzernamen und " #~ "Passwort müssen Sie einen Code " #~ "verwenden, den wir Ihnen zusenden" #~ msgid "Please enter your authentication code" #~ msgstr "Bitte geben Sie den Authentisierungscode ein" #~ msgid "Setup Unified Sign In options" #~ msgstr "Single-User-Login einrichten" #~ msgid "Please re-authenticate" #~ msgstr "Bitte neu authentisieren, um auf diese Seite zuzugreifen." #~ msgid "Please Enter Your Password" #~ msgstr "Bitte geben Sie Ihr Passwort ein" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Für diesen Benutzer ist kein Passwort gesetzt" #~ msgid "Invalid Token" #~ msgstr "Ungültiger Token" #~ msgid "Your token has been confirmed" #~ msgstr "Ihr Token wurde bestätigt" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ "Öffnen Sie die Authentisierungs-App z.B." #~ " auf Ihrem Smartphone und scannen Sie" #~ " den folgenden QR-Code (oder geben" #~ " Sie den unten stehenden Code ein)," #~ " um den Empfang von Anmelde-Codes " #~ "zu beginnen" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ "Nickname: \"%s\" Verwendung: \"%s\" Transport:" #~ " \"%s\" Entdeckbar: \"%s\" Zuletzt " #~ "verwendet am: %s" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Das Passwort wurde nicht innerhalb von" #~ " %(within)s zurückgesetzt. Eine neue " #~ "Anleitung wurde an %(email)s gesendet." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Die E-Mail-Adresse wurden nicht " #~ "innerhalb von %(within)s bestätigt. Neue " #~ "Instruktionen wurden an %(email)s gesendet." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ "Sie sind nicht angemeldet. Bitte geben" #~ " Sie die korrekten Zugangsdaten ein." #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "Mögliche Anmeldemöglichkeiten:" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ "Um die Anmeldung abzuschließen, geben " #~ "Sie bitte den Code ein, den wir" #~ " an Ihre E-Mail-Adresse geschickt " #~ "haben" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "An welche Telefonnummer sollen wir den Code senden?" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ "Wir haben eine E-Mail von Ihnen " #~ "erhalten, um Ihnen bei der " #~ "Wiederherstellung Ihres Kontos zu unterstützen" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ "Es trat ein Fehler bei der " #~ "Kommunikation mit dem OAuth-Provider " #~ "auf. Bitte versuchen Sie es erneut." #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "" #~ "Vielen Dank. Um Ihre E-Mail-Adresse " #~ "%(email)s zu bestätigen, klicken Sie " #~ "bitte auf den Link in der E-Mail," #~ " die wir gerade an Sie gesendet " #~ "haben." #~ msgid "Two-factor Login" #~ msgstr "Zwei-Faktor-Anmeldung" #~ msgid "Two-factor Rescue" #~ msgstr "Zwei-Faktor-Wiederherstellung" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Bitte neu authentisieren, um auf diese Seite zuzugreifen." #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Zwei-Faktor-Authentisierung wurde deaktiviert" #~ msgid "Disable two factor authentication" #~ msgstr "Deaktiviere Zwei-Faktor-Authentisierung" #~ msgid "Two Factor Setup" #~ msgstr "Zwei-Faktor-Einrichtung" #~ msgid "Sign in with " #~ msgstr "Anmelden mit " #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "Zwei-Faktor-Methode auswählen" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ "Zwei-Faktor-Authentisierung bietet eine " #~ "zusätzliche Sicherheitsebene für Ihr " #~ "Benutzerkonto" #~ msgid "Two factor authentication code" #~ msgstr "Zwei-Faktor-Authentisierungscode" #~ msgid "Two-factor Authentication" #~ msgstr "Zwei-Faktor-Authentisierung" #~ msgid "Please Reauthenticate" #~ msgstr "Bitte authentisieren Sie sich erneut" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "Bitte authentisieren Sie sich mit Ihrem WebAuthn-Sicherheitsschlüssel" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Passwort ändern" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Mein Konto bestätigen" #~ msgid "You can log into your account using the following code:" #~ msgstr "Sie können sich mit folgendem Code in Ihr Benutzerkonto anmelden:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "Sie können sich mit folgendem Code in Ihr Benutzerkonto anmelden:" #~ msgid "Or use the link below:" #~ msgstr "Oder nutzen Sie den folgenden Link:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Bitte die E-Mail-Adresse durch den Link unten bestätigen:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "" #~ "Bestätigen Sie Ihre E-Mail-Adresse, " #~ "indem Sie auf den Link unten " #~ "klicken:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "Wenn Sie Ihr Passwort vergessen habne, können Sie es" #~ msgid " here." #~ msgstr " hier zurücksetzen." #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ "Sollten Sie Ihr Passwort vergessen " #~ "haben, können Sie es unter folgendem " #~ "Link zurücksetzen:" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Code zur Anmeldung: %(code)s" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ "Der WebAuthn-Vorgang muss innerhalb von" #~ " %(within)s beendet werden. Bitte erneut" #~ " versuchen." #~ msgid "Nickname for new credential is required." #~ msgstr "Zum Setzen eines neuen Webauthn-Tokens ist ein Nickname erforderlich." #~ msgid "%(name)s is already associated with a credential." #~ msgstr "%(name)s ist bereits mit einem Webauthn-Token verknüpft." #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "Der Token '%(name)s' wurde entfernt." #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "Der WebAuthn-Token '%(name)s' wurde hinzugefügt." #~ msgid "WebAuthn credential id already registered." #~ msgstr "WebAuthn-Token-ID wurde bereits eingetragen." #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "Nicht eingetragene WebAuthn-Token-ID." #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "WebAuthn-Token gehört zu keinem Benutzer." #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "Konnte WebAuthn-Token nicht verifizieren: %(cause)s." #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "WebAuthn-Token ist für diese Verwendung nicht eingetragen." #~ msgid "webauthn" #~ msgstr "WebAuthn" #~ msgid "WebAuthn Setup" #~ msgstr "WebAuthn-Einrichtung" #~ msgid "Forgot password" #~ msgstr "Passwort vergessen" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "Nutze WebAuthn zur Anmeldung" #~ msgid "Sign in with WebAuthn" #~ msgstr "Anmelden mit WebAuthn" #~ msgid "WebAuthn" #~ msgstr "WebAuthn" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "Diese Anwendung unterstützt WebAuthn-Sicherheitsschlüssel." #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ "Bitte nutzen Sie ein WebAuthn-" #~ "Sicherheitsschlüssel, um sich erneut zu " #~ "authentisieren" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "Neuen WebAuthn-Sicherheitsschlüssel einrichten" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ "Geben Sie zunächst einen eindeutigen " #~ "Namen für den neuen Sicherheitsschlüssel " #~ "ein:" #~ msgid "Currently registered security keys:" #~ msgstr "Bereits registrierte Sicherheitsschlüssel:" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "Lösche existierenden WebAuth-Sicherheitsschlüssel" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "Mit WebAuthn-Sicherheitsschlüssel anmelden" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "Verwenden Sie Ihren WebAuthn-Sicherheitsschlüssel als zweiten Faktor" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/es_ES/000077500000000000000000000000001511046741400232425ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/es_ES/LC_MESSAGES/000077500000000000000000000000001511046741400250275ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/es_ES/LC_MESSAGES/flask_security.po000066400000000000000000001413751511046741400304310ustar00rootroot00000000000000# Spanish (Spain) translations for Flask-Security. # Copyright (C) 2025 THE AUTHORS # This file is distributed under the same license as the Flask-Security # project. # Mauko Quiroga , 2017. # Martin Mozos , 2020. # Giorgio Stampa , 2025. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 5.6.2\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2025-11-09 19:15+0100\n" "Last-Translator: Giorgio Stampa \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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "Confirma tu nueva dirección de correo electrónico" #: flask_security/core.py:296 msgid "Login Required" msgstr "Inicio de sesión necesario" #: flask_security/core.py:297 msgid "Welcome" msgstr "Bienvenido·a" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Confirma tu correo electrónico" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Instrucciones para iniciar sesión" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "Tu contraseña ha sido cambiada" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Instrucciones de recuperación de contraseña" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "Tu nombre de usuario ha sido cambiado" #: flask_security/core.py:304 msgid "Your requested username" msgstr "Tu nombre de usuario solicitado" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "Inicio de sesión de dos factores" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "Recuperación de sesión de dos factores" #: flask_security/core.py:350 msgid "Verification Code" msgstr "Código de verificación" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "Entrada no apropiada para la API solicitada" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" "Fallo de autenticación - identidad o contraseña/código de acceso no " "válidos" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Si esa dirección de correo electrónico está en nuestro sistema, recibirás" " un correo electrónico con la descripción de como restablecer tu " "contraseña." #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "Si esa identidad está en nuestro sistema, se te envió un código." #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "No tienes permiso para consultar este recurso." #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "Debes autenticarte para acceder a este recurso." #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "Debes volver a autenticarte para acceder a este recurso" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" "Gracias. Para confirmar tu dirección de correo electrónico %(email)s, haz" " clic en el enlace del mensaje que acabamos de enviarte." #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Gracias. Tu correo electrónico ha sido confirmado." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "Tu correo electrónico ya ha sido confirmado." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Autentificador de confirmación inválido." #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s ya está asociado a una cuenta." #: flask_security/core.py:438 #, 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:444 #, python-format msgid "Identity %(id)s not registered" msgstr "Identidad %(id)s no registrada" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" "Se ha producido un error al comunicarse con el proveedor de OAuth: " "(%(exerror)s - %(exdesc)s). Por favor inténtalo de nuevo." #: flask_security/core.py:455 msgid "Password does not match" msgstr "La contraseña no coincide" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Las contraseñas no coinciden" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Las redirecciones a sitios web externos están prohibidas" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "Código de recuperación inválido" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "Aún no se han generado códigos de recuperación" #: flask_security/core.py:461 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" "Las instrucciones para restablecer tu contraseña se han enviado a " "%(email)s." #: flask_security/core.py:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "No has restablecido tu contraseña dentro de %(within)s. " #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Autentificador de restablecimiento de contraseña inválido." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "El correo electrónico requiere confirmación." #: flask_security/core.py:471 #, 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:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "No has confirmado tu correo electrónico dentro de %(within)s. " #: flask_security/core.py:480 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "No has iniciado sesión antes de %(within)s. Nuevas instrucciones para " "iniciar sesión se han enviado a %(email)s." #: flask_security/core.py:486 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Instrucciones para iniciar sesión se han enviado a %(email)s." #: flask_security/core.py:489 msgid "Invalid login token." msgstr "Autenticador de inicio de sesión inválido." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Cuenta deshabilitada." #: flask_security/core.py:491 msgid "Email not provided" msgstr "Correo electrónico no indicado" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Dirección de correo electrónico inválida" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "Código inválido" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Contraseña no indicada" #: flask_security/core.py:496 #, python-format msgid "Password must be at least %(length)s characters" msgstr "La contraseña debe tener al menos %(length)s caracteres" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "La contraseña no es lo suficientemente compleja" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "Contraseña en la lista de contraseñas violadas" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "No se ha podido contactar con el sitio de contraseñas violadas" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "El número de teléfono no es válido, p. ej. falta el código de país" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "Usuario·a especificado·a no existe" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Contraseña inválida" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "La contraseña o el código facilitado son inválidos" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Has iniciado sesión." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "¿Has olvidado tu contraseña?" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "Has restablecido tu contraseña y has iniciado sesión automáticamente." #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "Has restablecido tu contraseña. Autentícate con tu nueva contraseña." #: flask_security/core.py:526 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:529 msgid "You successfully changed your password." msgstr "Has cambiado tu contraseña." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Debes iniciar sesión para poder acceder a esta página." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Debes iniciar sesión nuevamente para poder acceder a esta página." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "Reautenticación exitosa" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "Solo puedes acceder a este recurso si no has iniciado sesión." #: flask_security/core.py:537 msgid "Code has been sent." msgstr "El código se ha enviado." #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "No se pudo enviar el código. Por favor inténtalo de nuevo más tarde" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "Tu código ha sido confirmado" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "Has cambiado tu método de dos factores." #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "Actualmente no tienes permisos para acceder a esta página" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "El método marcado no es válido" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "Has deshabilitado la autorización de dos factores." #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" "La configuración debe completarse dentro de %(within)s. Por favor vuelve " "a empezar." #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "Opciones de inicio de sesión activas actualmente: %(method_list)s." #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "El método solicitado no es válido" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "El inicio de sesión unificado se ha configurado correctamente" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "Debes especificar una identidad válida para iniciar sesión" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "Utiliza este código para iniciar sesión: %(code)s" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "Has cambiado tu nombre de usuario" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "El nombre de usuario debe tener al menos %(min)d caracteres y menos de " "%(max)d caracteres" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "El nombre de usuario contiene caracteres no admitidos" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "El nombre de usuario solo puede contener letras y números" #: flask_security/core.py:586 msgid "Username not provided" msgstr "Nombre de usuario no proporcionado" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s ya está asociado a una cuenta." #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" "Las operaciones con llaves de acceso deben completarse dentro de " "%(within)s. Por favor vuelve a empezar." #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "Se requiere un nombre para la nueva llave de acceso." #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "%(name)s ya está asociado a una llave de acceso." #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s no registrado con el usuario actual." #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "Se ha eliminado la llave de acceso con nombre: %(name)s" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "Se ha añadido la llave de acceso con nombre: %(name)s" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "Identificador de llave de acceso ya registrado." #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "Identificador de llave de acceso no registrado." #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "La llave de acceso no pertenece a ningún usuario." #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "No se pudo verificar la llave de acceso: %(cause)s." #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "Llave de acceso no registrada para este uso (primaria o secundaria)" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "La credencial de usuario no coincide" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" "La confirmación debe completarse dentro de %(within)s. Por favor vuelve a" " empezar." #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "Cambio de dirección de correo electrónico confirmado" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" "Las instrucciones para confirmar tu dirección de correo electrónico se " "han enviado a %(email)s." #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "Si registrado, tu nombre de usuario se enviará a tu correo electrónico." #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" "Configurar con una aplicación de autenticación (p.ej. google, lastpass, " "authy)" #: flask_security/forms.py:64 msgid "Change Method" msgstr "Método de cambio" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Cambiar la contraseña" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "Código de autenticación" #: flask_security/forms.py:67 msgid "Delete" msgstr "Eliminar" #: flask_security/forms.py:68 msgid "Email Address" msgstr "Correo electrónico" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "Configurar con correo electrónico" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "Error(es)" #: flask_security/forms.py:71 msgid "Identity" msgstr "Identidad" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Iniciar sesión" #: flask_security/forms.py:73 msgid "New Password" msgstr "Nueva contraseña" #: flask_security/forms.py:74 msgid "Passcode" msgstr "Código de acceso" #: flask_security/forms.py:75 msgid "Password" msgstr "Contraseña" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "Número de teléfono" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Recuperar contraseña" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "Recuperar nombre de usuario" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registrarse" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Recordarme" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Restablecer contraseña" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Escribir contraseña nuevamente" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Reenviar instrucciones de confirmación" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Enviar enlace para iniciar sesión" #: flask_security/forms.py:85 msgid "Send Code" msgstr "Enviar código" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Iniciar sesión" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "Configurar con SMS" #: flask_security/forms.py:88 msgid "Submit" msgstr "Enviar" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "Confirmar código" #: flask_security/forms.py:90 msgid "Username" msgstr "Nombre de usuario" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "Verificar contraseña" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "Google Authenticator" #: flask_security/forms.py:97 msgid "authenticator" msgstr "aplicación de autenticación" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "correo electrónico" #: flask_security/forms.py:100 msgid "SMS" msgstr "SMS" #: flask_security/forms.py:101 msgid "password" msgstr "contraseña" #: flask_security/forms.py:102 msgid "passkey" msgstr "llave de acceso" #: flask_security/forms.py:103 msgid "none" msgstr "ninguno" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "Métodos disponibles" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "Deshabilitar la autenticación de dos factores" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "¿Problemas para acceder a tu cuenta/has perdido tu dispositivo móvil?" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "Contactar con el administrador" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "Mostrar los códigos de recuperación" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "Generar nuevos códigos de recuperación" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "Código de recuperación" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Métodos de segundo factor disponibles:" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "Selecciona" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "Enviar el código por correo electrónico" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "Usar código de recuperación descargado anteriormente" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "Código o contraseña" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "Vía correo electrónico" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "Vía SMS" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "Configurar una opción de inicio de sesión adicional" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "Eliminar la opción de inicio de sesión activa" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "Nombre" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "Uso" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "Uso como factor de autenticación primario" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "Uso como factor de autenticación secundario" #: flask_security/webauthn.py:225 msgid "Start" msgstr "Iniciar" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menú" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "Cerrar sesión" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "Cambiar el correo electrónico registrado" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "Cambiar el nombre de usuario" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "Configuración de dos factores" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "Configuración de inicio de sesión unificado" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "Configuración de llave de acceso" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "Inicio de sesión unificado" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Confirmar cuenta" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "Cambiar el correo electrónico" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" "Un mensaje de confirmación se enviará a esta nueva dirección de correo " "electrónico." #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "Actualmente no tienes una contraseña - esto añadirá una." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "El nombre de usuario actual es: %(username)s" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Enviar instrucciones para restablecer la contraseña" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "o" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "Usar una llave de acceso para iniciar sesión" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "Iniciar sesión con llave de acceso" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "Iniciar sesión con social OAuth" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "Iniciar sesión con %(provider)s" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Introducir código de recuperación" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Códigos de recuperación" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Asegúrate de copiarlos y guardarlos en un lugar seguro. Cada código sólo " "puede utilizarse una vez." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "Generar nuevos códigos de recuperación" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "Recuperación del nombre de usuario" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Restablecer contraseña" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Reenviar instrucciones de confirmación" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "Selecciona un método de dos factores" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" "La autenticación de dos factores añade una capa adicional de seguridad a " "tu cuenta" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "Además de tu nombre de usuario y contraseña, deberás utilizar un código." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Método de dos factores actualmente configurado: %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Abre una aplicación de autenticación en tu dispositivo y escanea el " "siguiente código QR (o ingresa manualmente el código que aparece a " "continuación) para comenzar a recibir códigos:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "Código de autenticación de dos factores" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "Introduce el código para completar la configuración" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "introduce el código numérico" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Esta aplicación permite establecer códigos de recuperación." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Se pueden configurar aquí." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "Llaves de acceso" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "Esta aplicación es compatible con llaves de acceso." #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "Autenticación de dos factores" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "Introduce tu código de autenticación generado a través de: %(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" 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:24 msgid "An email 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/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Configurar el inicio de sesión unificado" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Código QR sin contraseña" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "No se ha habilitado ningún método, no hay nada que configurar" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "Introduce el código aquí para completar la configuración" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Solicitar que se envíe un código de un solo uso" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "Vuelve a autenticarte" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Se envió el código" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "Utilizar una llave de acceso para reautenticarse" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "Configurar una nueva llave de acceso" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "Comienza por proporcionar un nombre único para tu nueva llave de acceso:" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "Llaves de acceso actualmente registradas:" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "Nombre: \"%s\" Uso: \"%s\" Transportes: \"%s\" Descubrible: \"%s\" Tipo " "de dispositivo: \"%s\" ¿Con copia de respaldo? \"%s\" Utilizado por " "última vez: \"%s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "Eliminar una llave de acceso existente" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "Iniciar sesión con una llave de acceso" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "Iniciar sesión con una llave de acceso" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "Utiliza una llave de acceso como segundo factor" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "Vuelve a autenticarte con una llave de acceso" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" "Usa este enlace para confirmar tu dirección de " "correo electrónico." #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "Este enlace caducará dentro de %(within)s." #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "Tu correo electrónico registrado actualmente es %(email)s." #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "Usa %(link)s para confirmar tu dirección de correo electrónico." #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Tu contraseña ha sido cambiada." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Si no has cambiado tú la contraseña," #: flask_security/templates/security/email/change_notice.html:4 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 has cambiado tú la contraseña, haz clic en el enlace de abajo para " "restablecerla." #: flask_security/templates/security/email/change_username_notice.html:1 msgid "Your username has been changed." msgstr "Tu nombre de usuario ha sido cambiado." #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" "Usa este enlace para confirmar tu " "dirección de correo electrónico." #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" "Usa %(confirmation_link)s para confirmar tu dirección de correo " "electrónico." #: 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "¡Bienvenido·a %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Inicia sesión a través del enlace de abajo:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Inicia sesión ahora" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Haz clic aquí para restablecer la contraseña" #: flask_security/templates/security/email/reset_instructions.txt:8 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:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "¡Bienvenido·a %(username)s!" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" "Puedes iniciar sesión en tu cuenta utilizando el siguiente código: " "%(token)s" #: 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 "no puede acceder a la cuenta de correo" #: flask_security/templates/security/email/us_instructions.html:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" "Puedes iniciar sesión en tu cuenta utilizando el siguiente código: " "%(token)s" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "O puedes utilizar este enlace: Autenticarse" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" "Puedes iniciar sesión en tu cuenta utilizando el siguiente código: " "%(token)s." #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "O puedes utilizar este enlace: %(login_link)s" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "Hola," #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "Recientemente has solicitado recuperar tu nombre de usuario." #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "Tu nombre de usuario es: %(username)s" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "Si no has iniciado esta solicitud, puedes ignorar este mensaje." #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "¡Hola %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Alguien (¿tú?) ha intentado registrar este correo electrónico, que ya " "está en nuestro sistema." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" "Esta cuenta también tiene asociado el siguiente nombre de usuario: " "%(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" "Puedes usar este enlace para restablecer " "tu contraseña." #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" "Aún no has confirmado tu dirección de correo electrónico - utiliza este enlace para hacerlo ahora." #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" "Esta cuenta también tiene asociado el siguiente nombre de usuario: " "%(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "Puedes usar este enlace %(reset_link)s para restablecer tu contraseña." #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" "Aún no has confirmado tu dirección de correo electrónico - utiliza este " "enlace: %(confirmation_link)s para hacerlo ahora." #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Has intentado registrarte con un nombre de usuario \"%(username)s\" que " "ya está asociado a otra cuenta." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" "Por favor reinicia el proceso de registro con un nombre de usuario " "diferente." #~ msgid "Two-factor Login" #~ msgstr "Inicio de sesión de dos factores" #~ msgid "Two-factor Rescue" #~ msgstr "Recuperación de sesión de dos factores" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Debes volver a autenticarte para acceder a este recurso" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Has deshabilitado la autorización de dos factores." #~ msgid "Disable two factor authentication" #~ msgstr "Deshabilitar la autenticación de dos factores" #~ msgid "Two Factor Setup" #~ msgstr "Configuración de dos factores" #~ msgid "Sign in with " #~ msgstr "Iniciar sesión con " #~ msgid "Select Two Factor Method" #~ msgstr "Selecciona un método de dos factores" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ "La autenticación de dos factores añade" #~ " una capa adicional de seguridad a" #~ " tu cuenta" #~ msgid "Two factor authentication code" #~ msgstr "Código de autenticación de dos factores" #~ msgid "Two-factor Authentication" #~ msgstr "Autenticación de dos factores" #~ msgid "Please Reauthenticate" #~ msgstr "Por favor vuelve a autenticarte" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "Por favor vuelve a autenticarte con tu clave de seguridad de WebAuthn" #~ msgid "Change email" #~ msgstr "Cambiar el correo electrónico" #~ msgid "Change password" #~ msgstr "Cambiar la contraseña" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ "Confirma tu nueva dirección de correo" #~ " electrónico a través del enlace de" #~ " abajo:" #~ msgid "Confirm my new email" #~ msgstr "Confirma mi nuevo correo electrónico" #~ msgid "Confirm my account" #~ msgstr "Confirmar mi cuenta" #~ msgid "You can log into your account using the following code:" #~ msgstr "Puedes iniciar sesión en tu cuenta utilizando el siguiente código:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "Puedes iniciar sesión en tu cuenta con el siguiente código:" #~ msgid "Or use the link below:" #~ msgstr "O a través del enlace de abajo:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "Confirma tu nuevo correo electrónico a través del enlace de abajo:" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Confirma tu correo electrónico a través del enlace de abajo:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Puedes confirmar tu correo electrónico a través del enlace de abajo:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "Si has olvidado tu contraseña, puedes restablecerla" #~ msgid " here." #~ msgstr " aquí." #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ "Si has olvidado tu contraseña, puedes" #~ " restablecerla a través del siguiente " #~ "enlace:" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Utiliza este código para iniciar sesión: %(code)s." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ "Si has olvidado tu contraseña, puedes" #~ " restablecerla " #~ "aquí." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ "Si has olvidado tu contraseña, puedes" #~ " restablecerla a través del siguiente " #~ "enlace: %(recovery_link)s" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ "La operación de WebAuthn debe " #~ "completarse dentro de %(within)s. Por " #~ "favor vuelve a empezar." #~ msgid "Nickname for new credential is required." #~ msgstr "Se requiere un nombre para la nueva credencial." #~ msgid "%(name)s is already associated with a credential." #~ msgstr "%(name)s ya está asociado a una credencial." #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "Se ha eliminado la credencial WebAuthn con nombre: %(name)s" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "Se ha añadido la credencial WebAuthn con nombre: %(name)s" #~ msgid "WebAuthn credential id already registered." #~ msgstr "Identificador de credencial de WebAuthn ya registrado." #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "Identificador de credencial de WebAuthn no registrado." #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "La credencial WebAuthn no pertenece a ningún usuario." #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "No se pudo verificar la credencial WebAuthn: %(cause)s." #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "Credencial no registrada para este uso (primaria o secundaria)" #~ msgid "webauthn" #~ msgstr "webauthn" #~ msgid "WebAuthn Setup" #~ msgstr "Configuración de WebAuthn" #~ msgid "Forgot password" #~ msgstr "Contraseña olvidada" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "Usar WebAuthn para iniciar sesión" #~ msgid "Sign in with WebAuthn" #~ msgstr "Iniciar sesión con WebAuthn" #~ msgid "WebAuthn" #~ msgstr "WebAuthn" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "Esta aplicación es compatible con las claves de seguridad de WebAuthn." #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "Utilizar una clave de seguridad de WebAuthn para reautenticarse" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "Configurar una nueva clave de seguridad de WebAuthn" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ "Comienza por proporcionar un nombre " #~ "único para tu nueva clave de " #~ "seguridad:" #~ msgid "Currently registered security keys:" #~ msgstr "Claves de seguridad actualmente registradas:" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "Eliminar la clave de seguridad de WebAuthn existente" #~ msgid "WebAuthn Security Key" #~ msgstr "Clave de seguridad de WebAuthn" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "Iniciar sesión con la clave de seguridad de WebAuthn" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "Utiliza tu clave de seguridad de WebAuthn como segundo factor" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "Vuelve a autenticarte con tu clave de seguridad de WebAuthn" flask-security-5.7.1/flask_security/translations/eu_ES/000077500000000000000000000000001511046741400232445ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/eu_ES/LC_MESSAGES/000077500000000000000000000000001511046741400250315ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/eu_ES/LC_MESSAGES/flask_security.po000066400000000000000000001235371511046741400304330ustar00rootroot00000000000000# 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: 2025-11-03 19:21-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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Saioa hasi behar da" #: flask_security/core.py:297 msgid "Welcome" msgstr "Ongi etorri" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Mesedez berretsi zure posta elektronikoa" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Saioa hasteko argibideak" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "Zure pasahitza aldatu da" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Pasahitza berreskuratzeko argibideak" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "Egiaztapen kodea" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "Sarrera ez da egokia eskatutako APIarentzat" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "Ez duzu baliabide hau kontsultatzeko baimenik." #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Eskerrik asko. Zure posta elektronikoa berretsi da." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "Zure posta elektronikoa dagoeneko baieztatuta dago." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Berrespen token baliogabea." #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s dagoeneko kontu batekin lotuta dago." #: flask_security/core.py:438 #, 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:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "Pasahitza ez dator bat" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Pasahitzak ez datoz bat" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Domeinutik kanpoko birbideratzeak debekatuta daude" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, 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:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Pasahitza berrezartzeko token baliogabea." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "Mezu elektronikoak berrespena behar du." #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Baieztapen argibideak %(email)s helbidera bidali dira." #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, 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:486 #, 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:489 msgid "Invalid login token." msgstr "Saioa hasteko token baliogabea." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Kontua desgaituta dago." #: flask_security/core.py:491 msgid "Email not provided" msgstr "Helbide elektronikoa beharrezkoa da" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Helbide elektroniko baliogabea" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "Kode baliogabea" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Pasahitza beharrezkoa da" #: flask_security/core.py:496 #, python-format msgid "Password must be at least %(length)s characters" msgstr "Pasahitzak gutxienez %(length)s karaktere izan behar ditu" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "Pasahitza ez da nahikoa konplexua" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "Pasahitza urratutako zerrendan" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "Ezin izan da urratutako pasahitzen iturburuarekin harremanetan jarri" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "Telefono zenbakiak ez du balio, baliteke herrialde kodea faltatzea" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "Zehaztutako erabiltzea ez da existitzen" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Pasahitz okerra" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "Zehaztutako pasahitzak edo kodeak ez du balio" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Behar bezala hasi duzu saioa." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Pasahitza ahaztua?" #: flask_security/core.py:513 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:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 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:529 msgid "You successfully changed your password." msgstr "Pasahitza behar bezala aldatu duzu." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Hasi saioa orri honetara sartzeko." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Mesedez, berriro autentifikatu orri honetara sartzeko." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "Berautentifikatzea osatu da" #: flask_security/core.py:534 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:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "Ezin izan da kodea bidali. Saiatu berriro geroago" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "Bi faktoretako metodoa ondo aldatu duzu." #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "Une honetan ez duzu baimenik orrialde honetara sartzeko" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "Markatutako metodoak ez du balio" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, 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:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "Eskatutako metodoak ez du balio" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "Bateratutako saio hasierarako konfigurazioa ongi egin da" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "Saioa hasteko identitate baliagarria zehaztu behar duzu" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "Erabiltzaile izenak gutxienez %(min)d karaktere eta %(max)d karaktere " "baino gutxiago izan behar ditu" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "Erabiltzaile izenak legez kanpoko karaktereak ditu" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "Erabiltzaile izenak letrak eta zenbakiak soilik izan ditzake" #: flask_security/core.py:586 msgid "Username not provided" msgstr "Erabiltzaile izena ez da eman" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s dagoeneko kontu batekin lotuta dago." #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 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/forms.py:64 msgid "Change Method" msgstr "Aldatzeko metodoa" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Aldatu pasahitza" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "Autentifikazio kodea" #: flask_security/forms.py:67 msgid "Delete" msgstr "" #: flask_security/forms.py:68 msgid "Email Address" msgstr "Posta elektronikoa" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "Konfiguratu posta elektronikoa erabiliz" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "Errorea(k)" #: flask_security/forms.py:71 msgid "Identity" msgstr "Identitatea" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Hasi saioa" #: flask_security/forms.py:73 msgid "New Password" msgstr "Pasahitz berria" #: flask_security/forms.py:74 msgid "Passcode" msgstr "Pasakodea" #: flask_security/forms.py:75 msgid "Password" msgstr "Pasahitza" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "Telefono zenbakia" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Pasahitza berreskuratu" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Izena eman" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Gogorarazi" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Pasahitza berrezarri" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Idatzi berriro pasahitza" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Bidali berrespen argibideak" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Bidali saioa hasteko esteka" #: flask_security/forms.py:85 msgid "Send Code" msgstr "Bidali kodea" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Hasi saioa" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "Konfiguratu SMS bidez" #: flask_security/forms.py:88 msgid "Submit" msgstr "Bidali" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "Bidali kodea" #: flask_security/forms.py:90 msgid "Username" msgstr "Erabiltzaile izena" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "Pasahitza ziurtatu" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "Eskuragarri dauden metodoak" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "Kodea edo pasahitza" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "Posta elektronikoaren bidez" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "SMS bidez" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:225 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menua" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "Saio hasiera bateratua" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Berretsi kontua" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Bidali pasahitza berrezartzeko argibideak" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Pasahitza berrezarri" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Bidali berrespen argibideak" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Ireki autentifikazio aplikazio bat zure gailuan eta eskaneatu QR kode hau" " (edo idatzi beheko kodea eskuz) kodeak jasotzen hasteko:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 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:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Pasahitzik gabeko QR kodea" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Ez da metodorik gaitu, ez dago ezer konfiguratzeko" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: 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:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Kodea bidali da" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Zure pasahitza aldatu da." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Pasahitza aldatu ez baduzu," #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Ongi etorri %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:4 msgid "Login now" msgstr "Hasi saioa orain" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Egin klik hemen zure pasahitza berrezartzeko" #: flask_security/templates/security/email/reset_instructions.txt:8 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:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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 "ezin da posta kontura sartu" #: flask_security/templates/security/email/us_instructions.html:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "Username not allowed" #~ msgstr "Erabiltzaile izena ez da onartzen" #~ 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" #~ msgid "Please enter your authentication code" #~ msgstr "Mesedez, sartu autentifikazio kodea" #~ msgid "Setup Unified Sign In options" #~ msgstr "Konfiguratu saioa hasteko aukera bateratuak" #~ msgid "Please re-authenticate" #~ msgstr "Mesedez, berriro autentifikatu" #~ msgid "Please Enter Your Password" #~ msgstr "Mesedez, sartu zure pasahitza" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Ez da pasahitzik ezarri erabiltzaile honentzat" #~ msgid "Invalid Token" #~ msgstr "Token baliogabea" #~ msgid "Your token has been confirmed" #~ msgstr "Zure token baieztatu da" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ "Ireki autentifikazio aplikazio bat zure " #~ "gailuan eta eskaneatu QR kode hau " #~ "(edo idatzi beheko kodea eskuz) " #~ "pasakodeak jasotzen hasteko:" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ 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." #~ 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." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "Ez zaude autentifikatuta. Mesedez, eman egiaztagiri zuzenak." #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ "Saioa hasten bukatzeko, idatzi zure " #~ "posta elektronikora bidalitako kodea" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "Zein telefono zenbakiri bidali beharko genioke kodea?" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "Zure eskaera kontua berrezartzeko mezu elektroniko bat bidali ziguten" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "Eskerrik asko. Baieztapen argibideak %(email)s helbidera bidali dira." #~ msgid "Two-factor Login" #~ msgstr "Bi faktoreko saioa hastea" #~ msgid "Two-factor Rescue" #~ msgstr "Bi faktoreko saioa berreskuratzea" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Baliabide honetara sartzeko berriro autentifikatu behar duzu" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Bi faktoreren baimena behar bezala desgaitu duzu." #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ 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" #~ msgid "Two factor authentication code" #~ msgstr "Bi faktoretako autentifikazio kodea" #~ msgid "Two-factor Authentication" #~ msgstr "Bi faktoretako autentifikazioa" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Aldatu pasahitza" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Berretsi nire kontua" #~ msgid "You can log into your account using the following code:" #~ msgstr "Zure kontuan saioa has dezakezu kode hau erabiliz:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "Zure kontuan saioa has dezakezu kode hau erabiliz:" #~ msgid "Or use the link below:" #~ msgstr "Edo erabili beheko esteka:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Mesedez, berretsi zure posta elektronikoa beheko estekaren bidez:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Zure posta elektronikoa beheko estekaren bidez baiezta dezakezu:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Erabili kode hau saioa hasteko: %(code)s." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Pasahitza ahaztua" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/flask_security.pot000066400000000000000000000747741511046741400260420ustar00rootroot00000000000000# Translations template for Flask-Security. # Copyright (C) 2025 ORGANIZATION # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2025. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: Flask-Security 5.7.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2025-11-03 19:21-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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "" #: flask_security/core.py:297 msgid "Welcome" msgstr "" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "" #: flask_security/core.py:299 msgid "Login instructions" msgstr "" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "" #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "" #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "" #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "" #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "" #: flask_security/core.py:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" #: flask_security/core.py:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "" #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "" #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "" #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, 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:486 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "" #: flask_security/core.py:489 msgid "Invalid login token." msgstr "" #: flask_security/core.py:490 msgid "Account is disabled." msgstr "" #: flask_security/core.py:491 msgid "Email not provided" msgstr "" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "" #: flask_security/core.py:494 msgid "Password not provided" msgstr "" #: flask_security/core.py:496 #, python-format msgid "Password must be at least %(length)s characters" msgstr "" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "" #: flask_security/core.py:507 msgid "Invalid password" msgstr "" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "" #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 msgid "Your new password must be different than your previous password." msgstr "" #: flask_security/core.py:529 msgid "You successfully changed your password." msgstr "" #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "" #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "" #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:586 msgid "Username not provided" msgstr "" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:64 msgid "Change Method" msgstr "" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:67 msgid "Delete" msgstr "" #: flask_security/forms.py:68 msgid "Email Address" msgstr "" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "" #: flask_security/forms.py:71 msgid "Identity" msgstr "" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "" #: flask_security/forms.py:73 msgid "New Password" msgstr "" #: flask_security/forms.py:74 msgid "Passcode" msgstr "" #: flask_security/forms.py:75 msgid "Password" msgstr "" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "" #: flask_security/forms.py:85 msgid "Send Code" msgstr "" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:88 msgid "Submit" msgstr "" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "" #: flask_security/forms.py:90 msgid "Username" msgstr "" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:225 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "" #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "" #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:4 msgid "Login now" msgstr "" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" flask-security-5.7.1/flask_security/translations/fr_FR/000077500000000000000000000000001511046741400232425ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/fr_FR/LC_MESSAGES/000077500000000000000000000000001511046741400250275ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.po000066400000000000000000001221511511046741400304200ustar00rootroot00000000000000# French (France) translations for Flask-Security. # Copyright (C) 2017 CERN # This file is distributed under the same license as the Flask-Security # project. # Alexandre Bulté , 2017. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2017-06-08 10:13+0200\n" "Last-Translator: Alexandre Bulté \n" "Language: fr_FR\n" "Language-Team: fr_FR \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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Connexion requise" #: flask_security/core.py:297 msgid "Welcome" msgstr "Bienvenue" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Merci de confirmer votre adresse email" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Instructions de connexion" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "Votre mot de passe a été changé" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Instructions de réinitialisation de votre mot de passe" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 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:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "Merci de vous reconnecter pour accéder à cette page." #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Merci. Votre adresse email a été confirmée." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "Votre adresse email a déjà été confirmée." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Token de confirmation non valide." #: flask_security/core.py:433 #, 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:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "Le mot de passe ne correspond pas" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Les mots de passe ne correspondent pas" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Les redirections en dehors du domaine sont interdites" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, 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:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Token de réinitialisation non valide." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "Une confirmation de l'adresse email est requise." #: flask_security/core.py:471 #, 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:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Vous ne vous êtes pas connecté dans l'intervalle requis (%(within)s)De " "nouvelles instructions ont été envoyées à %(email)s." #: flask_security/core.py:486 #, 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:489 msgid "Invalid login token." msgstr "Token de connexion non valide." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Le compte est désactivé." #: flask_security/core.py:491 msgid "Email not provided" msgstr "Merci d'indiquer une adresse email" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Adresse email non valide" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "Code invalide" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Merci d'indiquer un mot de passe" #: flask_security/core.py:496 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" msgstr "Le mot de passe doit comporter au moins %(length)s caractères" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "Numéro de téléphone non valide, par ex. code pays manquant" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "Cet utilisateur n'existe pas" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Mot de passe non valide" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "Le mot de passe ou le code soumis n'est pas valide" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Vous êtes bien connecté." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Mot de passe oublié ?" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Vous avez bien réinitialisé votre mot de passe et avez été " "automatiquement connecté." #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 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:529 msgid "You successfully changed your password." msgstr "Vous avez bien changé votre mot de passe." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Merci de vous connecter pour accéder à cette page." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Merci de vous reconnecter pour accéder à cette page." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "Réauthentification réussie" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "Vous avez réussi à modifier votre méthode à deux facteurs." #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "Options de connexion actuellement actives : %(method_list)s." #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "Configuration de la connexion unifiée réussie" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "Vous devez spécifier une identité valide pour vous connecter" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:586 msgid "Username not provided" msgstr "" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" "Configuration à l'aide d'une application d'authentification (par exemple," " google, lastpass, authy)" #: flask_security/forms.py:64 msgid "Change Method" msgstr "Changer de méthode" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Changer le mot de passe" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "Code d'Identification" #: flask_security/forms.py:67 msgid "Delete" msgstr "" #: flask_security/forms.py:68 msgid "Email Address" msgstr "Adresse email" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "" #: flask_security/forms.py:71 msgid "Identity" msgstr "Identité" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Connexion" #: flask_security/forms.py:73 msgid "New Password" msgstr "Nouveau mot de passe" #: flask_security/forms.py:74 msgid "Passcode" msgstr "Code d'accès" #: flask_security/forms.py:75 msgid "Password" msgstr "Mot de passe" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Récupérer le mot de passe" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Inscription" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Se souvenir de moi" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Réinitialiser le mot de passe" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Confirmer le mot de passe" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Renvoyer les instructions de confirmation" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Envoyer le lien de connexion" #: flask_security/forms.py:85 msgid "Send Code" msgstr "" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Sign In" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "Configurer par SMS" #: flask_security/forms.py:88 msgid "Submit" msgstr "Nous faire parvenir" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "Soumettre le code" #: flask_security/forms.py:90 msgid "Username" msgstr "" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "Authentificateur Google" #: flask_security/forms.py:97 msgid "authenticator" msgstr "authentificateur" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "e-mail" #: flask_security/forms.py:100 msgid "SMS" msgstr "SMS" #: flask_security/forms.py:101 msgid "password" msgstr "mot de passe" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "aucune" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "Méthodes disponibles" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "Désactiver l'authentification à deux facteurs" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Méthodes de second facteur disponibles:" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "Code ou mot de passe" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "Par SMS" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "Configurer une option de connexion supplémentaire" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "Surnom" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "Utiliser comme premier facteur d'authentification" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "Utiliser comme facteur d'authentification secondaire" #: flask_security/webauthn.py:225 msgid "Start" msgstr "Démarrer" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "Déconnexion" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "Configuration à deux facteurs" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "Configuration de la connexion unifiée" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "Connexion unifiée" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Confirmer le compte" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Envoyer les instructions de réinitialisation de mot de passe" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Réinitialiser le mot de passe" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Renvoyer les instructions de confirmation" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" "En plus de votre nom d'utilisateur et de votre mot de passe, vous devrez " "utiliser un code." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Méthode à deux facteurs actuellement configurée : %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Vous pouvez les paramétrer ici." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "Authentification à deux facteurs" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "Veuillez saisir votre code d'authentification généré via: %(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Configurer la connexion unifiée" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Demander l'envoi d'un code à usage unique" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Le code a été envoyé" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: 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/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Si vous n'avez pas changé votre mot de passe," #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" "Utilisez ce lien pour confirmer " "votre adresse e-mail adresse." #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Bienvenue %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Se connecter maintenant" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Cliquez pour réinitialiser votre mot de passe" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Cet utilisateur n'a pas de mot de passe" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Vous n'avez pas réinitialisé votre mot" #~ " de passe dans l'intervalle requis " #~ "(%(within)s)De nouvelles instructions ont été" #~ " envoyées à %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Vous n'avez pas confirmé votre adresse" #~ " email dans l'intervalle requis " #~ "(%(within)s)De nouvelles instructions ont été" #~ " envoyées à %(email)s." #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ "Pour terminer la connexion, veuillez " #~ "saisir le code envoyé à votre " #~ "messagerie" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "entrez le code" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "Merci. Les instructions de confirmation ont été envoyées à %(email)s." #~ msgid "Two-factor Login" #~ msgstr "" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Merci de vous reconnecter pour accéder à cette page." #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "Désactiver l'authentification à deux facteurs" #~ msgid "Two Factor Setup" #~ msgstr "Configuration à deux facteurs" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "Sélectionnez la méthode à deux facteurs" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ "L'authentification à deux facteurs ajoute " #~ "une couche de sécurité supplémentaire à" #~ " votre compte" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "Authentification à deux facteurs" #~ msgid "Please Reauthenticate" #~ msgstr "Merci de vous reconnecter pour accéder à cette page." #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ "Veuillez vous authentifier à nouveau à" #~ " l'aide de votre clé de sécurité " #~ "WebAuthn" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Changer de mot de passe" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Confirmer mon compte" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Merci de confirmer votre adresse email via le lien ci-dessous:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Vous pouvez confirmer votre votre adresse email via le lien ci-dessous:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "Le surnom du nouvel identifiant est requis." #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "Identifiant non enregistré pour cet usage (premier ou secondaire)" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "Configuration de WebAuthn" #~ msgid "Forgot password" #~ msgstr "Mot de passe oublié" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "Utiliser WebAuthn pour se connecter" #~ msgid "Sign in with WebAuthn" #~ msgstr "Connectez-vous avec WebAuthn" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "Cette application prend en charge les clés de sécurité WebAuthn." #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "Utiliser une clé de sécurité WebAuthn pour réauthentifier" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "Configurer une nouvelle clé de sécurité WebAuthn" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ "Commencez par fournir un nom unique " #~ "pour votre nouvelle clé de sécurité:" #~ msgid "Currently registered security keys:" #~ msgstr "Configurer une nouvelle clé de sécurité WebAuthn" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "Supprimer la clé de sécurité WebAuthn existante" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "Se connecter à l'aide de la clé de sécurité WebAuthn" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "Utilisez votre clé de sécurité WebAuthn comme deuxième facteur" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/hu_HU/000077500000000000000000000000001511046741400232545ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/hu_HU/LC_MESSAGES/000077500000000000000000000000001511046741400250415ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/hu_HU/LC_MESSAGES/flask_security.po000066400000000000000000001305461511046741400304410ustar00rootroot00000000000000# Hungarian (Hungary) translations for Flask-Security. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR ritt.alex@gmail.com, 2022. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 4.0.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: hu_HU\n" "Language-Team: hu_HU \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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Bejelentkezés szükséges" #: flask_security/core.py:297 msgid "Welcome" msgstr "Üdvözöljük" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Kérjük, erősítse meg e-mail címét" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Bejelentkezési utasítások" #: flask_security/core.py:300 #: 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 jelszava visszaállításra került" #: flask_security/core.py:301 msgid "Your password has been changed" msgstr "A jelszava megváltozott" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Jelszó visszaállítási utasítások" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "Ellenőrző kód" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "A bemenet nem megfelelő a kért API-hoz" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "Sikertelen hitelesítés - azonosító vagy jelszó/kód érvénytelen" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Ha ez az e-mail cím szerepel a rendszerünkben, akkor kapni fog egy " "e-mailt a jelszó visszaállítási menetéről." #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "Ha ez az azonosító szerepel a rendszerünkben, akkor kódot küldtünk." #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "Nincs jogosultsága ennek az erőforrásnak a megtekintéséhez." #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Köszönjük. E-mail címét megerősítettük." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "Az e-mail címét már megerősítették." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Érvénytelen megerősítő token." #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s már társítva van egy fiókhoz." #: flask_security/core.py:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "A \"%(attr)s\" azonosító attribútum \"%(value)s\" értékkel már társítva " "van egy fiókhoz." #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "A jelszó nem egyezik" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "A jelszavak nem egyeznek" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "A tartományon kívüli átirányítások tilosak" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "A helyreállítási kód érvénytelen" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" "A jelszó visszaállítására vonatkozó utasításokat elküldtük a következő " "címre: %(email)s." #: flask_security/core.py:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Érvénytelen jelszó-visszaállítási token." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "Az e-mail megerősítést igényel." #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "A megerősítő utasításokat elküldtük a következő címre: %(email)s." #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Nem jelentkezett be %(within)s-en belül. Új bejelentkezési utasításokat " "küldtünk a következő címre: %(email)s." #: flask_security/core.py:486 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "" "A bejelentkezéshez szükséges utasításokat elküldtük a következő címre: " "%(email)s." #: flask_security/core.py:489 msgid "Invalid login token." msgstr "Érvénytelen bejelentkezési token." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "A fiók le van tiltva." #: flask_security/core.py:491 msgid "Email not provided" msgstr "E-mail nincs megadva" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Érvénytelen e-mail cím" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "Érvénytelen kód" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Nincs megadva a jelszó" #: flask_security/core.py:496 #, python-format msgid "Password must be at least %(length)s characters" msgstr "A jelszónak legalább %(length)s karakterből kell állnia" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "A jelszó nem elég összetett" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "A telefonszám nem érvényes, pl. hiányzik az országkód" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "A megadott felhasználó nem létezik" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Érvénytelen jelszó" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "A beküldött jelszó vagy kód érvénytelen" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Sikeresen bejelentkezett." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Elfelejtette jelszavát?" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "Sikeresen visszaállította jelszavát, és automatikusan be is jelentkezett." #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 msgid "Your new password must be different than your previous password." msgstr "Az új jelszavának különböznie kell a korábbi jelszavától." #: flask_security/core.py:529 msgid "You successfully changed your password." msgstr "Sikeresen megváltoztatta a jelszavát." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Kérjük, jelentkezzen be az oldal eléréséhez." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Kérjük, hitelesítse újra az oldal eléréséhez." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "Az újrahitelesítés sikeres" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "\"Csak akkor érheti el ezt a végpontot, ha nincs bejelentkezve." #: flask_security/core.py:537 msgid "Code has been sent." msgstr "A kód elküldve." #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "Nem sikerült elküldeni a kódot. Kérjük, próbálja újra később." #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "A kódod megerősítésre került" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "Sikeresen megváltoztatta a kétfaktoros hitelesítést." #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "Jelenleg nincs jogosultsága az oldal eléréséhez" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "A megjelölt metódus nem érvényes" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "A telepítést %(within)s időn belül be kell fejezni. Kezdje újra." #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "A kért metódus nem érvényes" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "Az egységes bejelentkezés beállítása sikeres" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "A bejelentkezéshez érvényes azonositót kell megadnia" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "A felhasználónévnek legalább %(min)d karakterből és kevesebb mint %(max)d" " karakterből kell állnia" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "A felhasználónév illegális karaktereket tartalmaz" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "A felhasználónév csak betűket és számokat tartalmazhat" #: flask_security/core.py:586 msgid "Username not provided" msgstr "A felhasználónév nincs megadva" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s már hozzá van rendelve egy fiókhoz." #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s nincs regisztrálva az aktuális felhasználónál." #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "Beállítás hitelesítő alkalmazás segítségével (pl. google, lastpass, authy)" #: flask_security/forms.py:64 msgid "Change Method" msgstr "Módszer módosítása" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Jelszó módosítása" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "Hitelesítési kód" #: flask_security/forms.py:67 msgid "Delete" msgstr "Törlés" #: flask_security/forms.py:68 msgid "Email Address" msgstr "E-mail" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "Beállítás e-mail használatával" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "Hiba(k)" #: flask_security/forms.py:71 msgid "Identity" msgstr "Azonosító" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Bejelentkezés" #: flask_security/forms.py:73 msgid "New Password" msgstr "Új jelszó" #: flask_security/forms.py:74 msgid "Passcode" msgstr "Kód" #: flask_security/forms.py:75 msgid "Password" msgstr "Jelszó" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "Telefonszám" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Jelszó helyreállítása" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Regisztráció" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Emlékezz rám" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Jelszó visszaállítása" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Jelszó megerősítése" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Megerősítő utasítások újraküldése" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Bejelentkezési link küldése" #: flask_security/forms.py:85 msgid "Send Code" msgstr "Kód küldése" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Bejelentkezés" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "Beállítás SMS-sel" #: flask_security/forms.py:88 msgid "Submit" msgstr "Küldés" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "Kód beküldése" #: flask_security/forms.py:90 msgid "Username" msgstr "Felhasználónév" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "Jelszó ellenőrzése" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "Elérhető módszerek" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "Helyreállítási kódok megjelenítése" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "Új helyreállítási kódok generálása" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "Helyreállítási kód" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Elérhető második faktoros módszerek:" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "Kiválasztás" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "Kód vagy jelszó" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "E-mailben" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "SMS-ben" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "Kiegészítő bejelentkezési lehetőség beállítása" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "Az aktív bejelentkezési opció törlése" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "Becenév" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "Használat" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "Használat első hitelesítési lépésként" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "Használat másodlagos hitelesítési lépésként" #: flask_security/webauthn.py:225 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menü" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "Kijelentkezés" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "Egységes bejelentkezés beállítása" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "Egységes bejelentkezés" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Fiók megerősítése" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "Jelenleg nincs jelszava – ez hozzáad egyet." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Jelszó visszaállítási utasítások küldése" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "vagy" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Adja meg a helyreállítási kódot" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Helyreállítási kódok" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Mindenképpen másolja át ezeket, és tárolja biztonságos helyen. Minden kód" " csak egyszer használható." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Jelszó visszaállítása" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Megerősítő utasítások újraküldése" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "A felhasználóneve és a jelszava mellett egy kódot is kell használnia." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Nyisson meg egy hitelesítő alkalmazást eszközén, és olvassa be a " "következő QR-kódot (vagy írja be kézzel az alábbi kódot), hogy megkezdje " "a kódok fogadását:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Ez az alkalmazás támogatja a helyreállítási kódok beállítását." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Itt állíthatja be." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" "Kérjük, adja meg hitelesítési kódját, amelyet a következővel generált: " "%(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "A hitelesítési kódot elküldtük az Ön e-mail címére" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Egységes bejelentkezés beállítása" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Jelszó nélküli QRCode" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Nincs engedélyezve metódus – nincs mit beállítani" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "A beállítás befejezéséhez írja be ide a kódot" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Kérje egyszeri kód küldését" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "A kód elküldve" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "Becenév: \"%s\" Használat: \"%s\" Szállítás: \"%s\" Felfedezhető: \"%s\" " "Eszköz típusa: \"%s\" Biztonsági mentés készült? \"%s\" Utolsó használat:" " %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "A jelszava megváltozott." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Ha nem változtatta meg jelszavát," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "kattints ide a visszaállításhoz" #: 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 "" "Ha nem változtatta meg jelszavát, kattintson az alábbi linkre a " "visszaállításhoz." #: flask_security/templates/security/email/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Üdvözöljük %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Az alábbi linken keresztül jelentkezhet be fiókjába:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Bejelentkezés most" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Kattintson ide a jelszó visszaállításához" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "Kattintson az alábbi linkre a jelszó visszaállításához:" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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 "nem tud hozzáférni a mail fiókhoz" #: flask_security/templates/security/email/us_instructions.html:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "Szia %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Valaki megpróbálta regisztrálni ezt az e-mailt - amely már a " "rendszerünkben van." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "Ehhez a fiókhoz a következő felhasználónév is tartozik: %(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "Ehhez a fiókhoz a következő felhasználónév is tartozik: \"%(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Olyan felhasználónévvel próbált meg regisztrálni: \"%(username)s\", amely" " már társítva van egy másik fiókhoz." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" "Kérjük, indítsa újra a regisztrációs folyamatot egy másik " "felhasználónévvel." #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Nem állította vissza jelszavát %(within)s " #~ "időn belül. Új utasításokat küldtünk a" #~ " következő címre: %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Nem erősítette meg e-mail-címét " #~ "%(within)s-en belül. Az e-mail megerősítéséhez" #~ " új utasításokat küldtünk a következő " #~ "címre: %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "Ön nincs hitelesítve. Kérjük, adja meg a megfelelő hitelesítő adatokat." #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "A bejelentkezés befejezéséhez adja meg az e-mail címére küldött kódot" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "Melyik telefonszámra küldjük a kódot?" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ "E-mailt küldtünk nekünk az alkalmazás " #~ "fiókjának visszaállítása érdekében" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "" #~ "Köszönjük. A megerősítő utasításokat elküldtük" #~ " a következő címre: %(email)s." #~ msgid "Two-factor Login" #~ msgstr "Kétfaktoros Bejelentkezés" #~ msgid "Two-factor Rescue" #~ msgstr "Kétfaktoros Visszaállítás" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "A végpont eléréséhez újra hitelesíteni kell" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Sikeresen letiltotta a kétfaktoros hitelesítést." #~ msgid "Disable two factor authentication" #~ msgstr "Kétfaktoros hitelesítés letiltása" #~ msgid "Two Factor Setup" #~ msgstr "Kétfaktoros beállítás" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "Kétfaktoros módszer kiválasztása" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "A kétfaktoros hitelesítés extra biztonsági réteget ad fiókjához" #~ msgid "Two factor authentication code" #~ msgstr "Kétfaktoros hitelesítési kód" #~ msgid "Two-factor Authentication" #~ msgstr "Kétfaktoros Hitelesítés" #~ msgid "Please Reauthenticate" #~ msgstr "Kérem hitelesítse újra" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "Kérjük, hitelesítsen újra a WebAuthn biztonsági kulcs használatával" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Jelszó módosítása" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Fiók megerősítése" #~ msgid "You can log into your account using the following code:" #~ msgstr "A következő kóddal jelentkezhet be fiókjába:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "A következő kóddal jelentkezhet be fiókjába:" #~ msgid "Or use the link below:" #~ msgstr "Vagy használja az alábbi linket:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Kérjük, erősítse meg e-mail címét az alábbi linken keresztül:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Megerősítheti e-mailjét az alábbi linken keresztül:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "Ha elfelejtette jelszavát, visszaállíthatja" #~ msgid " here." #~ msgstr " ezen a linken." #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "Ha elfelejtette jelszavát, a következő linken visszaállíthatja:" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Használja ezt a kódot a bejelentkezéshez: %(code)s." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ "A WebAuthn műveletet %(within)s időn " #~ "belül be kell fejezni. Kezdje újra." #~ msgid "Nickname for new credential is required." #~ msgstr "Az új hitelesítő adatokhoz becenév szükséges." #~ msgid "%(name)s is already associated with a credential." #~ msgstr "%(name)s már hozzá van rendelve egy hitelesítő adathoz." #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ "A WebAuthn hitelesítő adatok sikeresen " #~ "törölve a következő névvel: %(name)s" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ "Sikeresen hozzáadva a WebAuthn hitelesítő " #~ "adatot a következő névvel: %(name)s" #~ msgid "WebAuthn credential id already registered." #~ msgstr "A WebAuthn hitelesítő adatazonosítója már regisztrálva." #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "Nem regisztrált WebAuthn hitelesítő adat azonosítója." #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "A WebAuthn hitelesítő adatok nem tartoznak egyetlen felhasználóhoz sem." #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "Nem sikerült ellenőrizni a WebAuthn hitelesítő adatait: %(cause)s." #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ "A hitelesítő adat nincs regisztrálva " #~ "ehhez a felhasználáshoz (első vagy " #~ "másodlagos)" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "\"WebAuthn beállítása" #~ msgid "Forgot password" #~ msgstr "Elfelejtett jelszó" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "A WebAuthn használata a bejelentkezéshez" #~ msgid "Sign in with WebAuthn" #~ msgstr "Bejelentkezés WebAuthn segítségével" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "Ez az alkalmazás támogatja a WebAuthn biztonsági kulcsokat." #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "Használjon WebAuthn biztonsági kulcsot az újrahitelesítéshez" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "Új WebAuthn biztonsági kulcs beállítása" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "Először adjon meg egy egyedi nevet az új biztonsági kulcsnak:" #~ msgid "Currently registered security keys:" #~ msgstr "Jelenleg regisztrált biztonsági kulcsok:" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "Meglévő WebAuthn biztonsági kulcs törlése" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "Bejelentkezés WebAuthn biztonsági kulcs használatával" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "Használja a WebAuthn biztonsági kulcsát második lépésként" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/hy_AM/000077500000000000000000000000001511046741400232415ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/hy_AM/LC_MESSAGES/000077500000000000000000000000001511046741400250265ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/hy_AM/LC_MESSAGES/flask_security.po000066400000000000000000001453001511046741400304200ustar00rootroot00000000000000# 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: 2025-11-03 19:21-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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Անհրաժեշտ է մուտք գործել" #: flask_security/core.py:297 msgid "Welcome" msgstr "Բարի գալուստ" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Հաստատեք Ձեր էլ․փոստի հասցեն" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Մուտքի հրահանգներ" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "Ձեր գաղտնաբառը փոխվեց" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Գաղտնաբառի վերականգնման հրահանգներ" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "Վերահաստատման ծածկագիր" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "Մուտքը չի համապատասխանում հայցվող API֊ին" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "Նույնականացումը ձախողվեց. ինքնությունը կամ գաղտնաբառը/ծածկագիրը անվավեր է" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Եթե այդ էլ․փոստի հասցեն գտնվում է մեր համակարգում, դուք կստանաք նամակ, " "որտեղ նկարագրված է, թե ինչպես վերականգնել ձեր գաղտնաբառը:" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "Եթե այդ ինքնությունը մեր համակարգում է, ձեզ ծածկագիր է ուղարկվել:" #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "Դուք այս ռեսուրսը դիտելու թույլտվություն չունեք" #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Շնորհակալություն. Ձեր էլ․փոստի հասցեն հաստատվել է։" #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "Ձեր էլ․փոստի հասցեն արդեն հաստատված է։" #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Հաստատման տոկենը անվավեր է։" #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s արդեն կապված է այլ օգտահաշվի հետ։" #: flask_security/core.py:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "«%(value)s» արժեքով «%(attr)s» ինքնության հատկանիշն արդեն կապված է հաշվի " "հետ:" #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "«%(id)s» ինքնությունը գրանցված չէ" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "Գաղտնաբառը չի համապատասխանում" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Գաղտնաբառերը չեն համապատասխանում" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Դոմենից դուրս վերահղումներն արգելափակված են" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "Վերականգնման ծածկագիրը անվավեր է" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "Վերականգնման ծածկագրեր դեռ չեն ստեղծվել" #: flask_security/core.py:461 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Գաղտնաբառի վերականգնման հրահանգներն ուղարկվել են %(email)s հասցեին։" #: flask_security/core.py:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "Դուք չեք վերականգնել ձեր գաղտնաբառը %(within)s-ի ընթացքում:" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Գաղտնաբառի վերականգնման տոկենը անվավեր է։" #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "Էլ․փոստի հասցեն պետք է հաստատել։" #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Հաստատման հրահանգները ուղարկվել են %(email)s հասցեին։" #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "Դուք չեք հաստատել ձեր էլ․փոստի հասցեն %(within)s-ի ընթացքում:" #: flask_security/core.py:480 #, 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:486 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Մուտքի հրահանգները ուղարկվել են %(email)s հասցեին։" #: flask_security/core.py:489 msgid "Invalid login token." msgstr "Մուտքի անվավեր տոկեն։" #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Օգտահաշիվն արգելափակված է։" #: flask_security/core.py:491 msgid "Email not provided" msgstr "Էլ․փոստի հասցեն տրամադրված չէ" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Անվավեր էլ․փոստի հասցե" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "Անվավեր ծածկագիր" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Գաղտնաբառը տրամադրված չէ" #: flask_security/core.py:496 #, python-format msgid "Password must be at least %(length)s characters" msgstr "Գաղտնաբառը պետք է պարունակի առնվազն %(length)s նիշ" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "Գաղտնաբառը բավարար բարդ չէ" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "Գաղտնաբառը բացված գաղտնաբառերի ցուցակում է" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "Չհաջողվեց կապվել բացված գաղտնաբառերի կայքի հետ" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "Հեռախոսահամարը անվավեր է, օրինակ՝ բացակայում է երկրի կոդը" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "Նշված օգտատերը գոյություն չունի" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Անվավեր գաղտնաբառ" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "Ներկայացված գաղտնաբառը կամ ծածկագիրը վավեր չէ" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Դուք բարեհաջող մուտք էք գործել։" #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Մոռացել ե՞ք գաղտնաբառը" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Դուք հաջողությամբ վերականգնել եք ձեր գաղտնաբառը եւ մուտք էք գործել " "համակարգ ինքնաբերաբար։" #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" "Դուք հաջողությամբ վերականգնել եք ձեր գաղտնաբառը: Խնդրում ենք " "նույնականացնել՝ օգտագործելով ձեր նոր գաղտնաբառը:" #: flask_security/core.py:526 msgid "Your new password must be different than your previous password." msgstr "Ձեր նոր գաղտնաբառը պետք է տարբերվի նախկինից" #: flask_security/core.py:529 msgid "You successfully changed your password." msgstr "Դուք հաջողությամբ փոխեցիք ձեր գաղտնաբառը։" #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Մուտք գործեք այս էջից օգտվելու համար։" #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Այս էջ մուտք գործելու համար անհրաժեշտ է նորից նույնականացվել:" #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "Նույնականացումը հաջողված է" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "Դուք կարող եք մտնել այս վերջնակետ միայն այն ժամանակ, երբ մուտք չեք գործել" #: flask_security/core.py:537 msgid "Code has been sent." msgstr "Ծածկագիրը ուղարկվել է։" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "Չհաջողվեց ուղարկել ծածկագիրը: Խնդրում ենք փորձել ավելի ուշ" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "Ձեր ծածկագիրը հաստատվեց" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "Դուք հաջողությամբ փոխեցիք ձեր երկու֊գործոն տարբերակը" #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "Ներկայումս դուք չունեք այս էջ մուտք գործելու թույլտվություն" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "Նշված տարբերակը վավեր չէ" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "Կարգավորումը պետք է ավարտվի %(within)s ընթացքում։ Խնդրում ենք նորից սկսել։" #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "Հայցվող մեթոդը վավեր չէ" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "Միասնական մուտքի տեղադրումը հաջող է" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "Մուտք գործելու համար պետք է նշեք վավեր ինքնություն" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "Օգտանունը պետք է լինի առնվազն %(min)d նիշ և %(max)d նիշից պակաս" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "Օգտանունը պարունակում է անօրինական նիշեր" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "Օգտանունը կարող է պարունակել միայն տառեր և թվեր" #: flask_security/core.py:586 msgid "Username not provided" msgstr "Օգտանուն չի տրամադրվել" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s արդեն կապված է օգտահաշվի հետ:" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s գրանցված չէ ընթացիկ օգտատիրոջ մոտ:" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "Հավատարմագրերի օգտատիրոջ կապը չի համընկնում" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" "Կարգավորեք օգտագործելով վավերացման ծրագիր (ինչպիսիք են՝ google, lastpass," " authy)" #: flask_security/forms.py:64 msgid "Change Method" msgstr "Փոխել տարբերակը" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Փոխել գաղտնաբառը" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "Նույնականացման ծածկագիր" #: flask_security/forms.py:67 msgid "Delete" msgstr "Ջնջել" #: flask_security/forms.py:68 msgid "Email Address" msgstr "Էլեկտրոնային փոստի հասցե" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "Կարգավորեք էլ․փոստի միջոցով" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "Սխալ" #: flask_security/forms.py:71 msgid "Identity" msgstr "Ինքնություն" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Մուտք" #: flask_security/forms.py:73 msgid "New Password" msgstr "Նոր գաղտնաբառ" #: flask_security/forms.py:74 msgid "Passcode" msgstr "Գաղտնագիր" #: flask_security/forms.py:75 msgid "Password" msgstr "Գաղտնաբառ" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "Հեռախոսահամար" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Վերականգնել գաղտնաբառը" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Գրանցվել" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Հիշիր ինձ" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Զրոյացնել գաղտնաբառը" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Կրկին մուտքագրել գաղտնաբառը" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Վերստին հաստատեք ցուցումները" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Ուղարկել մուտքի հղումը" #: flask_security/forms.py:85 msgid "Send Code" msgstr "Ուղարկել ծածկագիր" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Մուտք գործել" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "Կարգավորեք օգտագործելով SMS" #: flask_security/forms.py:88 msgid "Submit" msgstr "Ներկայացնել" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "Ներկայացնել ծածկագիր" #: flask_security/forms.py:90 msgid "Username" msgstr "Օգտատեր" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "Ստուգեք գաղտնաբառը" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "Առկա տարբերակներ" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "Ցույց տալ վերականգնման ծածկագրերը" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "Ստեղծեք վերականգնման նոր ծածկագրեր" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "Վերականգնման ծածկագիր" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Երկրորդ գործոնի հասանելի մեթոդներ." #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "Ընտրել" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "Ծածկագիր կամ Գաղտնաբառ" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "Էլ․ Փոստով" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "SMS հաղորդագրությամբ" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "Կարգավորեք մուտքի լրացուցիչ տարբերակ" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "Ջնջել ակտիվ մուտքի տարբերակը" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "Մականուն" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "Կիրառում" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "Օգտագործեք որպես նույնականացման առաջին գործոն" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "Օգտագործեք որպես նույնականացման երկրորդական գործոն" #: flask_security/webauthn.py:225 msgid "Start" msgstr "Սկսել" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Ընտրացանք" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "Դուրս գալ" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "Միասնական մուտքի կարգավորում" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "Միասնական Մուտք" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Հաստատել օգտահաշիվը" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "Դուք ներկայումս գաղտնաբառ չունեք, սա կավելացվի:" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Ուղարկել գաղտնաբառի վերականգնման հրահանգները" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "կամ" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "Օգտագործել Social Oauth մուտք գործելու համար" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Մուտքագրեք վերականգնման ծածկագիրը" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Վերականգնման ծածկագրեր" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Համոզվեք, որ պատճենեք դրանք և պահեք ապահով տեղում: Յուրաքանչյուր ծածկագիր" " կարող է օգտագործվել միայն մեկ անգամ:" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "Ստեղծեք վերականգնման նոր ծածկագրեր" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Վերականգնել գաղտնաբառը" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Ուղարկել օգտահաշվի վերահաստատման հրահանգները" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "Բացի ձեր օգտանունից և գաղտնաբառից, դուք պետք է օգտագործեք ծածկագիր:" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Ներկայիս կարգավորված երկգործոն մեթոդը. %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Բացեք վավերացնող հավելվածը ձեր սարքում և սկանավորեք հետևյալ QRcode-ը (կամ" " մուտքագրեք ստորև նշված ծածկագիրը) ծածկագրեր ստանալու համար." #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Այս հավելվածն օժանդակում է վերականգնման ծածկագրերի կարգավորումը:" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Դուք կարող եք դրանք կարգավորել այստեղ:" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" "Խնդրում ենք մուտքագրել ձեր նույնականացման ծածկագիրը, որը ստեղծվել է " "%(method)s միջոցով." #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "Նույնականացման ծածկագիրն ուղարկվել է Ձեր էլ․հասցեին" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Կարգավորեք միասնական մուտքը" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Առանց գաղտնաբառի QRcode" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Ոչ մի տարբերակի հնարավորություն տրված չէ ֊ կարգաբերելու կարիք չկա" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "Մուտքագրեք ծածկագիրն այստեղ՝ կարգավորումն ավարտելու համար" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Հայցեք միանգամյա ծածկագրի ուղարկում" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Ծածկագիրն ուղարկվել է" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "Մականուն՝ \"%s\" Օգտագործում՝ \"%s\" Փոխադրումներ՝ \"%s\" Հայտնաբերելի՝ " "\"%s\" Սարքի տեսակ՝ \"%s\" Պահուստավորվե՞լ է: \"%s\" Վերջին անգամ " "օգտագործվել է՝ %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Ձեր գաղտնաբառը փոխվել է" #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Ձեր գաղտնաբառը եթե դուք չեք փոխել," #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Բարի գալուստ %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:4 msgid "Login now" msgstr "Մուտք գործել հիմա" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Ձեր գաղտնաբառը վերականգնելու համար սեղմեք այստեղ" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "Ձեր գաղտնաբառը վերականգնելու համար սեղմեք սեղմեք հղումին" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "Բարև %(email)s" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Ինչ-որ մեկը (դու՞ք) փորձել է գրանցել այս էլփոստը, որն արդեն մեր " "համակարգում է:" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "Այս հաշիվը ունի նաև հետևյալ օգտվողի անունը՝ կապված դրա հետ. %(username)s։" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "Այս հաշիվը ունի նաև հետևյալ օգտվողի անունը՝ կապված իրա հետ. %(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Դուք փորձել եք գրանցվել \"%(username)s\" օգտանունով, որը արդեն կապված է " "մեկ այլ հաշվի հետ:" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "Խնդրում ենք վերսկսել գրանցման գործընթացը այլ օգտվողի անունով:" #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "Դուք նույնականացված չեք: Խնդրում ենք մուտքագրել ճիշտ տվյալներ։" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "Ներկայում ակտիվ մուտքի տարբերակներ." #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ "Մուտքն ավարտելու համար մուտքագրեք ձեր " #~ "էլեկտրոնային հասցեին ուղարկված ծածկագիրը" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "Ո՞ր հեռախոսահամարին պետք է ուղարկել ծածկագիրը" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "Ձեր օգտահաշվի կիրառումը վերականգնելու համար մեզ նամակ է ուղարկվել" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "Oauth մատակարարի հետ կապի սխալ է տեղի ունեցել: Խնդրում եմ կրկին փորձեք." #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "Շնորհակալություն. Հաստատման հրահանգներն ուղարկվել են %(email)s հասցեին։" #~ msgid "Two-factor Login" #~ msgstr "Երկու գործոնով մուտք" #~ msgid "Two-factor Rescue" #~ msgstr "Երկու գործոնով վերականգնում" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Դուք պետք է կրկին նույնականցվեք որպեսզի այստեղ մուտք գործեք" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Դուք հաջողությամբ անջատել եք երկու գործոնի թույլտվությունը" #~ msgid "Disable two factor authentication" #~ msgstr "Անջատել երկու գործոնով նույնականացումը" #~ msgid "Two Factor Setup" #~ msgstr "Երկու գործոնի կարգավորում" #~ msgid "Sign in with " #~ msgstr "Մուտք գործեք " #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "Ընտրեք «երկու գործոնի» տարբերակը" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ "Երկու գործոնով նույնականացումը ձեր հաշվին " #~ "ավելացնում է անվտանգության լրացուցիչ շերտ" #~ msgid "Two factor authentication code" #~ msgstr "Երկու գործոն նույնականացման ծածկագիր" #~ msgid "Two-factor Authentication" #~ msgstr "Երկու գործոն նույնականացում" #~ msgid "Please Reauthenticate" #~ msgstr "Խնդրում եմ նորից վերանույնականցվեք:" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ "Խնդրում ենք վերանույնականցվեք՝ օգտագործելով " #~ "ձեր WebAuthn անվտանգության բանալին" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Փոխել գաղտնաբառը" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Հաստատել իմ օգտահաշիվը" #~ msgid "You can log into your account using the following code:" #~ msgstr "Դուք կարող եք մտնել Ձեր օգտահաշիվ՝ օգտագործելով հետևյալ ծածկագիրը." #~ msgid "You can sign into your account using the following code:" #~ msgstr "Կարող եք մուտք գործել Ձեր օգտահաշիվ՝ օգտագործելով հետևյալ ծածկագիրը." #~ msgid "Or use the link below:" #~ msgstr "Կամ օգտագործեք ստորեւ նշված հղումը" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Ստորև բերված հղումով հաստատեք ձեր էլ. փոստի հասցեն․" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Դուք կարող եք հաստատել ձեր էլ. փոստի հասցեն ստորև նշված հղումով." #~ msgid "If you forgot your password you can reset it" #~ msgstr "Եթե մոռացել եք ձեր գաղտնաբառը, կարող եք վերականգնել այն" #~ msgid " here." #~ msgstr " այստեղ։" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ "Եթե մոռացել եք ձեր գաղտնաբառը, կարող " #~ "եք վերականգնել այն հետևյալ հղումով." #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Մուտքի համար օգտագործեք այս ծածկագիրը․ %(code)s։" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ "WebAuthn գործողությունը պետք է ավարտվի " #~ "%(within)s։ Խնդրում եմ սկսել նորից:" #~ msgid "Nickname for new credential is required." #~ msgstr "Նոր հավատարմագրի մականունը պարտադիր է:" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "%(name)s-ն արդեն կապված է հավատարմագրի հետ:" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "%(name)s անունով WebAuthn հավատարմագիրը հաջողությամբ ջնջվեց" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "%(name)s անունով WebAuthn հավատարմագիրը հաջողությամբ ավելացվեց" #~ msgid "WebAuthn credential id already registered." #~ msgstr "WebAuthn հավատարմագրի ID-ն արդեն գրանցված է" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "Չգրանցված WebAuthn հավատարմագրի ID:" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "WebAuthn հավատարմագիրը չի պատկանում որևէ օգտվողի:" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "Չհաջողվեց հաստատել WebAuthn հավատարմագիրը՝ %(cause)s:" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ "Հավատարմագրերը գրանցված չեն այս օգտագործման" #~ " համար (առաջին կամ երկրորդական)" #~ msgid "webauthn" #~ msgstr "webauthn" #~ msgid "WebAuthn Setup" #~ msgstr "WebAuthn կարգավորում" #~ msgid "Forgot password" #~ msgstr "Մոռացել եք գաղտնաբառը" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "Օգտագործել WebAuthn մուտք գործելու համար" #~ msgid "Sign in with WebAuthn" #~ msgstr "Մուտք գործել WebAuthn֊ի միջոցով" #~ msgid "WebAuthn" #~ msgstr "WebAuthn" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "Այս հավելվածն օժանդակում է WebAuthn անվտանգության բանալիներին:" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "Վերանույնականցվելու համար օգտագործեք WebAuthn անվտանգության բանալին" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "Կարգավորեք նոր WebAuthn անվտանգության բանալի" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "Սկսեք ձեր նոր անվտանգության բանալու համար եզակի անուն տրամադրելով." #~ msgid "Currently registered security keys:" #~ msgstr "Ներկայումս գրանցված անվտանգության բանալիներ." #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "Ջնջել գոյություն ունեցող WebAuthn անվտանգության բանալին" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "Մուտք գործեք՝ օգտագործելով WebAuthn անվտանգության բանալին" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "Օգտագործեք ձեր WebAuthn անվտանգության բանալին որպես երկրորդ գործոն" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/is_IS/000077500000000000000000000000001511046741400232525ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/is_IS/LC_MESSAGES/000077500000000000000000000000001511046741400250375ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/is_IS/LC_MESSAGES/flask_security.po000066400000000000000000001147261511046741400304410ustar00rootroot00000000000000# Icelandic (Iceland) translations for Flask-Security. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2022. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 4.0.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2022-04-23 17:04+0000\n" "Last-Translator: \n" "Language: is_IS\n" "Language-Team: is_IS \n" "Plural-Forms: nplurals=2; plural=(n%10==1 && n%100!=11 ? 0 : 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Innskráningar er þörf" #: flask_security/core.py:297 msgid "Welcome" msgstr "Velkomin(n)" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Vinsamlegast staðfestu netfangið þitt" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Leiðbeiningar fyrir innskráningu" #: flask_security/core.py:300 #: 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 "Lykilorðið þitt hefur verið endurstillt" #: flask_security/core.py:301 msgid "Your password has been changed" msgstr "Lykilorðinu þínu hefur verið breytt" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Leiðbeiningar um endurstillingu lykilorðs" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "Staðfestingarkóði" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "Þú hefur ekki heimild til að skoða þessa auðlind." #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Þakka þér fyrir. Netfangið þitt hefur verið staðfest." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "Netfangið þitt hefur þegar verið staðfest." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Ógildur staðfestingarkóði." #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s er þegar í notkun á öðrum reikningi." #: flask_security/core.py:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "Lykilorðið er ekki eins" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Lykilorðin eru ekki eins" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" "Leiðbeiningar um hvernig þú getur endurstillt lykilorðið þitt hafa verið " "sendar á %(email)s." #: flask_security/core.py:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "" #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "Netfangið þarfnast staðfestingar." #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "" #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, 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:486 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "" #: flask_security/core.py:489 msgid "Invalid login token." msgstr "" #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Aðgangurinn er óvirkur." #: flask_security/core.py:491 msgid "Email not provided" msgstr "Netfang var ekki gefið upp" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Ógilt netfang" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "Ógildur kóði" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Lykilorð var ekki gefið upp" #: flask_security/core.py:496 #, python-format msgid "Password must be at least %(length)s characters" msgstr "Lykilorð verða að vera að minnsta kosti %(length)s stafir" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "Lykilorðið er ekki nógu flókið" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "Lykilorðið er á lista yfir brotin lykilorð" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "Gat ekki haft samband við síðu yfir brotin lykilorð" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "Símanúmerið er ekki gilt, t.d. vegna þess að landakóða vantar" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Ógilt lykilorð" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Þér tókst að skrá þig inn." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Gleymt lykilorð?" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Þér tókst að endurstilla lykilorðið þitt og þú hefur verið skráð(ur) inn " "sjálfkrafa." #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 msgid "Your new password must be different than your previous password." msgstr "Nýja lykilorðið þitt verður að vera öðruvísi en gamla lykilorðið." #: flask_security/core.py:529 msgid "You successfully changed your password." msgstr "Þér tókst að breyta lykilorðinu þínu." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Vinsamlegast skráðu þig inn til að opna þessa síðu." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "" #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "Notendanafnið verður að vera að minnsta kosti %(min)d stafir að lengd og " "styttra en %(max)d stafir" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "Notendanafnið inniheldur óleyfileg tákn" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "Notendanafnið má eingöngu innihalda bókstafi og tölustafi" #: flask_security/core.py:586 msgid "Username not provided" msgstr "Notendanafn var ekki gefið upp" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:64 msgid "Change Method" msgstr "Breyta um aðferð" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Breyta lykilorði" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:67 msgid "Delete" msgstr "Eyða" #: flask_security/forms.py:68 msgid "Email Address" msgstr "Netfang" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "Villur" #: flask_security/forms.py:71 msgid "Identity" msgstr "Auðkenni" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Innskráning" #: flask_security/forms.py:73 msgid "New Password" msgstr "Nýtt lykilorð" #: flask_security/forms.py:74 msgid "Passcode" msgstr "" #: flask_security/forms.py:75 msgid "Password" msgstr "Lykilorð" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "Símanúmer" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Endurheimta lykilorð" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Skrá mig" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Muna eftir mér" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Endurstilla lykilorð" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Endurtekið lykilorð" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Senda aftur leiðbeiningar um staðfestingu" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "" #: flask_security/forms.py:85 msgid "Send Code" msgstr "Senda kóða" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Innskráning" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:88 msgid "Submit" msgstr "Senda" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "Senda kóða" #: flask_security/forms.py:90 msgid "Username" msgstr "Notendanafn" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "Sannreyna lykilorð" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "Velja" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "Kóði eða lykilorð" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "Með tölvupósti" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "Með SMS skilaboði" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "Gælunafn" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "Notkun" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:225 msgid "Start" msgstr "Byrja" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Valmynd" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "Skrá út" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Staðfesta reikning" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Senda leiðbeiningar fyrir endurstillingu lykilorðs" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Endurstilla lykilorð" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Kóði hefur verið sendur" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Lykilorðinu þínu hefur verið breytt." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Ef þú breyttir ekki lykilorðinu þínu," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "smelltu hér til að endurstilla það" #: 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 "" "Ef þú breyttir ekki lykilorðinu þínu smelltu þá á hlekkinn til að " "endurstilla það." #: flask_security/templates/security/email/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Velkomin(n) %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:4 msgid "Login now" msgstr "Skrá inn núna" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Smelltu hér til að endurstilla lykilorðið þitt" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Það er ekkert lykilorð skráð fyrir þennan notanda" #~ msgid "Invalid Token" #~ msgstr "" #~ msgid "Your token has been confirmed" #~ msgstr "" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "" #~ "Þakka þér fyrir. Leiðbeiningar fyrir " #~ "staðfestingu hafa verið sendar á " #~ "%(email)s." #~ msgid "Two-factor Login" #~ msgstr "Tveggja þátta innskráning" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "Tveggja-þátta auðkenning" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Breyta lykilorði" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Staðfesta reikninginn minn" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Vinsamlegast staðfestu netfangið þitt á eftirfarandi slóð:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Gleymt lykilorð" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/it_IT/000077500000000000000000000000001511046741400232545ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/it_IT/LC_MESSAGES/000077500000000000000000000000001511046741400250415ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/it_IT/LC_MESSAGES/flask_security.po000066400000000000000000001361521511046741400304400ustar00rootroot00000000000000# Italian (Italy) translations for Flask-Security. # Copyright (C) 2025 THE AUTHORS # This file is distributed under the same license as the Flask-Security # project. # Giorgio Stampa , 2025. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 5.6.2\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2025-11-09 18:12+0100\n" "Last-Translator: Giorgio Stampa \n" "Language: it_IT\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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "Conferma il tuo nuovo indirizzo email" #: flask_security/core.py:296 msgid "Login Required" msgstr "Accesso richiesto" #: flask_security/core.py:297 msgid "Welcome" msgstr "Benvenuto/a" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Conferma la tua email" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Istruzioni per l'accesso" #: flask_security/core.py:300 #: 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 "La tua password è stata reimpostata" #: flask_security/core.py:301 msgid "Your password has been changed" msgstr "La tua password è stata cambiata" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Istruzioni per reimpostare la password" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "Il tuo nome utente è stato cambiato" #: flask_security/core.py:304 msgid "Your requested username" msgstr "Il tuo nome utente richiesto" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "Accesso a due fattori" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "Recupero dell'accesso a due fattori" #: flask_security/core.py:350 msgid "Verification Code" msgstr "Codice di verifica" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "Input non appropriato per l'API richiesta" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "Autenticazione fallita - identità o password/passcode non validi" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Se quell'indirizzo email è nel nostro sistema, riceverai un'email che " "descrive come reimpostare la tua password." #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "Se quell'identità è nel nostro sistema, ti è stato inviato un codice." #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "Non hai il permesso per visualizzare questa risorsa." #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "È necessario accedere per accedere a questa risorsa." #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "È necessario riautenticarsi per accedere a questa risorsa" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" "Grazie. Per confermare il tuo indirizzo email %(email)s segui il link nel" " messaggio che ti abbiamo appena inviato." #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Grazie. La tua email è stata confermata." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "La tua email è già stata confermata." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Token di conferma non valido." #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s è già associata ad un account." #: flask_security/core.py:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "L'attributo di identità '%(attr)s' con valore '%(value)s' è già associato" " ad un account." #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "Identità %(id)s non registrata" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" "Si è verificato un errore durante la comunicazione con il prestatore " "OAuth: (%(exerror)s - %(exdesc)s). Per favore prova di nuovo." #: flask_security/core.py:455 msgid "Password does not match" msgstr "La password non corrisponde" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Le password non corrispondono" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "I reindirizzamenti all'esterno del dominio sono vietati" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "Codice di ripristino non valido" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "Non sono ancora stati generati codici di recupero" #: flask_security/core.py:461 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Le istruzioni per reimpostare la password sono state inviate a %(email)s." #: flask_security/core.py:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "Non hai reimpostato la password entro %(within)s. " #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Token di reimpostazione della password non valido." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "L'email richiede conferma." #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Le istruzioni di conferma sono state inviate a %(email)s." #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "Non hai confermato la tua email entro %(within)s. " #: flask_security/core.py:480 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Non hai effettuato l'accesso entro %(within)s. Nuove istruzioni per il " "login sono state inviate a %(email)s." #: flask_security/core.py:486 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Le istruzioni per accedere sono state inviate a %(email)s." #: flask_security/core.py:489 msgid "Invalid login token." msgstr "Token di accesso non valido." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "L'account è disabilitato." #: flask_security/core.py:491 msgid "Email not provided" msgstr "Email non fornita" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Indirizzo email non valido" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "Codice non valido" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Password non fornita" #: flask_security/core.py:496 #, python-format msgid "Password must be at least %(length)s characters" msgstr "La password deve contenere almeno %(length)s caratteri" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "Password non abbastanza complessa" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "Password nell'elenco delle password violate" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "Impossibile contattare il sito delle password violate" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "" "Numero di telefono non valido, potrebbe mancare il corretto prefisso " "internazionale" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "L'utente specificato non esiste" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Password non valida" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "La password o il codice inviato non sono validi" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Hai effettuato l'accesso." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Password dimenticata?" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Hai reimpostato la tua password e l'accesso è stato effettuato " "automaticamente." #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" "Hai reimpostato la tua password. Autenticati utilizzando la nuova " "password." #: flask_security/core.py:526 msgid "Your new password must be different than your previous password." msgstr "La tua nuova password deve essere diversa dalla password precedente." #: flask_security/core.py:529 msgid "You successfully changed your password." msgstr "Hai cambiato la tua password." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Devi effettuare l'accesso per accedere a questa pagina." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Devi autenticarti nuovamente per accedere a questa pagina." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "Riautenticazione riuscita" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "Puoi accedere a questa risorsa solo se non hai effettuato l'accesso." #: flask_security/core.py:537 msgid "Code has been sent." msgstr "Il codice è stato inviato." #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "Impossibile inviare il codice. Per favore prova di nuovo più tardi" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "Il tuo codice è stato confermato" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "Hai cambiato il tuo metodo a due fattori." #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "Al momento non disponi dei permessi per accedere a questa pagina" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "Il metodo selezionato non è valido" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "Hai disabilitato l'autorizzazione a due fattori." #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" "La configurazione deve essere completata entro %(within)s. Per favore " "ricomincia da capo." #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "Opzioni di accesso attualmente attive: %(method_list)s." #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "Il metodo richiesto non è valido" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "Configurazione dell'accesso unificato riuscita" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "Devi specificare un'identità valida per accedere" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "Utilizza questo codice per accedere: %(code)s" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "Hai cambiato il tuo nome utente" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "Il nome utente deve essere composto da almeno %(min)d caratteri e meno di" " %(max)d caratteri" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "Il nome utente contiene caratteri non ammessi" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "Il nome utente può contenere solo lettere e numeri" #: flask_security/core.py:586 msgid "Username not provided" msgstr "Nome utente non fornito" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s è già associato ad un account." #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "Il nome per la nuova passkey è obbligatorio." #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "%(name)s è già associato ad una passkey." #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s non registrato con l'utente attuale." #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "Passkey con nome: %(name)s eliminata" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "Passkey con nome: %(name)s aggiunta" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "Passkey già registrata." #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "Passkey non registrata." #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "La passkey non appartiene a nessun utente." #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "Impossibile verificare la passkey: %(cause)s." #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "Passkey non registrata per questo uso (primaria o secondaria)" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "La credenziale dell'utente non corrisponde" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" "La conferma deve essere completata entro %(within)s. Per favore " "ricomincia." #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "Cambio di indirizzo email confermato" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" "Le istruzioni per confermare il tuo nuovo indirizzo email sono state " "inviate a %(email)s." #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "Se registrato, il nome utente verrà inviato al tuo indirizzo e-mail." #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" "Configurazione tramite app di autenticazione (ad es. google, lastpass, " "authy)" #: flask_security/forms.py:64 msgid "Change Method" msgstr "Cambia metodo" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Cambia password" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "Codice di autenticazione" #: flask_security/forms.py:67 msgid "Delete" msgstr "Elimina" #: flask_security/forms.py:68 msgid "Email Address" msgstr "Indirizzo email" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "Configurazione tramite email" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "Errore(i)" #: flask_security/forms.py:71 msgid "Identity" msgstr "Identità" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Accedi" #: flask_security/forms.py:73 msgid "New Password" msgstr "Nuova password" #: flask_security/forms.py:74 msgid "Passcode" msgstr "Codice di accesso" #: flask_security/forms.py:75 msgid "Password" msgstr "Password" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "Numero di telefono" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Recupera password" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "Recupera il nome utente" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registrati" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Ricordati di me" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Reimposta password" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Ridigita password" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Invia nuovamente istruzioni di conferma" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Invia link di accesso" #: flask_security/forms.py:85 msgid "Send Code" msgstr "Invia codice" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Accedi" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "Configurazione tramite SMS" #: flask_security/forms.py:88 msgid "Submit" msgstr "Invia" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "Conferma codice" #: flask_security/forms.py:90 msgid "Username" msgstr "Nome utente" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "Verifica password" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "Google Authenticator" #: flask_security/forms.py:97 msgid "authenticator" msgstr "app di autenticazione" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "email" #: flask_security/forms.py:100 msgid "SMS" msgstr "SMS" #: flask_security/forms.py:101 msgid "password" msgstr "password" #: flask_security/forms.py:102 msgid "passkey" msgstr "passkey" #: flask_security/forms.py:103 msgid "none" msgstr "nessuno" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "Metodi disponibili" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "Disabilitare l'autenticazione a due fattori" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "Problemi di accesso al tuo account?/Dispositivo mobile smarrito?" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "Contatta l'amministratore" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "Mostra i codici di ripristino" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "Genera nuovi codici di ripristino" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "Codice di ripristino" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Metodi di autenticazione a due fattori disponibili:" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "Seleziona" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "Invia codice tramite email" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "Utilizza codice di ripristino scaricato in precedenza" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "Codice o password" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "Tramite email" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "Tramite SMS" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "Imposta opzione di accesso aggiuntiva" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "Elimina l'opzione di accesso attiva" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "Nome" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "Utilizzo" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "Utilizzare come fattore di autenticazione primario" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "Utilizzare come fattore di autenticazione secondario" #: flask_security/webauthn.py:225 msgid "Start" msgstr "Inizio" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menù" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "Esci" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "Cambia email registrata" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "Cambia nome utente" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "Imposta autenticazione a due fattori" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "Imposta accesso unificato" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "Imposta passkey" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "Accesso unificato" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Conferma account" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "Cambia email" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "Un messaggio di conferma sarà mandato a questo nuovo indirizzo email." #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "Al momento non hai una password: ne verrà aggiunta una." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "Il nome utente attuale è: %(username)s" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Invia istruzioni per reimpostare la password" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "o" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "Usa una passkey per accedere" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "Accedi con una passkey" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "Usa social OAuth per accedere" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "Accedi con %(provider)s" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Inserisci codice di ripristino" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Codici di ripristino" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Assicurati di copiarli e conservarli in un luogo sicuro. Ogni codice può " "essere utilizzato una sola volta." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "Genera nuovi codici di ripristino" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "Recupero del nome utente" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Reimposta password" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Invia nuovamente istruzioni di conferma" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "Seleziona metodo a due fattori" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" "L'autenticazione a due fattori aggiunge un ulteriore livello di sicurezza" " al tuo account" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "Oltre al nome utente e alla password, dovrai utilizzare un codice." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Metodo a due fattori attualmente attivo: %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Apri un'app di autenticazione sul tuo dispositivo e scansiona il seguente" " codice QR (o inserisci manualmente il codice sottostante) per iniziare a" " ricevere i codici:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "Codice di autenticazione a due fattori" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "Inserisci il codice per completare la configurazione" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "inserisci il codice numerico" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Questa applicazione supporta l'impostazione dei codici di ripristino." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Si possono impostare qui." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "Passkeys" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "Questa applicazione supporta le passkey." #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "Autenticazione a due fattori" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "Inserisci il tuo codice di autenticazione generato tramite: %(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "Il codice per l'autenticazione è stato inviato al tuo indirizzo email" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "Ci è stata inviata un'email per reimpostare l'account dell'applicazione" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Configurazione dell'accesso unificato" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Codice QR senza password" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Nessun metodo è stato abilitato - niente da impostare" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "Inserisci qui il codice per completare la configurazione" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Richiedere l'invio del codice monouso" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "Autenticati nuovamente" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Il codice è stato inviato" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "Utilizzare una passkey per autenticarsi nuovamente" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "Imposta una nuova passkey" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "Inizia col fornire un nome univoco per la tua nuova passkey:" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "Passkeys attualmente registrate:" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "Nome: \"%s\" Utilizzo: \"%s\" Trasporti: \"%s\" Rilevabile: \"%s\" Tipo " "di dispositivo: \"%s\" Backup? \"%s\" Ultimo utilizzo il: %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "Elimina una passkey esistente" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "Accedi utilizzando una passkey" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "Accedi utilizzando una passkey" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "Utilizza una passkey come secondo fattore" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "Autenticati nuovamente utilizzando una passkey" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" "Usa questo link per confermare il tuo nuovo " "indirizzo email." #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "Questo link scadrá entro %(within)s." #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "La tua email registrata attualmente è %(email)s." #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "Usa %(link)s per confermare il tuo nuovo indirizzo email." #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "La tua password è stata cambiata." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Se non hai cambiato tu la password," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "clicca qui per reimpostarla" #: 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 "" "Se non hai cambiato tu la password, clicca sul link sottostante per " "reimpostarla." #: flask_security/templates/security/email/change_username_notice.html:1 msgid "Your username has been changed." msgstr "Il tuo nome utente è stato cambiato." #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" "Usa questo link per confermare il " "tuo nuovo indirizzo email." #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "Usa %(confirmation_link)s per confermare il tuo nuovo indirizzo email." #: 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Benvenuto/a %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Puoi accedere al tuo account tramite il link sottostante:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Accedi adesso" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Clicca qui per reimpostare la password" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "Clicca sul link sottostante per reimpostare la password:" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "Benvenuto/a %(username)s!" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "Puoi accedere al tuo account utilizzando il seguente codice: %(token)s" #: 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 "impossibile accedere all'account email" #: flask_security/templates/security/email/us_instructions.html:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "Puoi accedere al tuo account utilizzando il seguente codice: %(token)s" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "Oppure puoi usare questo link: Accedi" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "Puoi accedere al tuo account utilizzando il seguente codice: %(token)s." #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "Oppure puoi usare questo link: %(login_link)s" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "Ciao," #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "Di recente hai chiesto di recuperare il tuo nome utente." #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "Il tuo nome utente è: %(username)s" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" "Se non hai avviato tu questa richiesta, puoi tranquillamente ignorare " "questo messaggio." #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "Ciao %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Qualcuno (tu?) ha provato a registrare questa email, che è già nel nostro" " sistema." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "A questo account è associato anche il seguente nome utente: %(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" "Puoi usare questo link per reimpostare la " "tua password." #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" "Non hai ancora confermato il tuo indirizzo e-mail - usa questo link per farlo adesso." #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "A questo account è associato anche il seguente nome utente: %(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "Puoi usare questo link %(reset_link)s per reimpostare la tua password." #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" "Non hai ancora confermato il tuo indirizzo e-mail - usa questo link: " "%(confirmation_link)s per farlo adesso." #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Hai tentato di registrarti con un nome utente \"%(username)s\" che è già " "associato ad un altro account." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" "Per favore riavvia il processo di registrazione con un nome utente " "diverso." #~ msgid "Two-factor Login" #~ msgstr "Accesso a due fattori" #~ msgid "Two-factor Rescue" #~ msgstr "Recupero dell'accesso a due fattori" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "È necessario riautenticarsi per accedere a questa risorsa" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Hai disabilitato l'autorizzazione a due fattori." #~ msgid "Disable two factor authentication" #~ msgstr "Disabilitare l'autenticazione a due fattori" #~ msgid "Two Factor Setup" #~ msgstr "Imposta autenticazione a due fattori" #~ msgid "Sign in with " #~ msgstr "Accedi con " #~ msgid "Select Two Factor Method" #~ msgstr "Seleziona metodo a due fattori" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ "L'autenticazione a due fattori aggiunge " #~ "un ulteriore livello di sicurezza al " #~ "tuo account" #~ msgid "Two factor authentication code" #~ msgstr "Codice di autenticazione a due fattori" #~ msgid "Two-factor Authentication" #~ msgstr "Autenticazione a due fattori" #~ msgid "Please Reauthenticate" #~ msgstr "Per favore autenticati nuovamente" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ "Per favore autenticati nuovamente utilizzando" #~ " la chiave di sicurezza WebAuthn" #~ msgid "Change email" #~ msgstr "Cambia email" #~ msgid "Change password" #~ msgstr "Cambia password" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "Conferma il tuo nuovo indirizzo email tramite il link sottostante:" #~ msgid "Confirm my new email" #~ msgstr "Conferma la mia nuova email" #~ msgid "Confirm my account" #~ msgstr "Conferma il mio account" #~ msgid "You can log into your account using the following code:" #~ msgstr "Puoi accedere al tuo account utilizzando il seguente codice:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "Puoi accedere al tuo account con il seguente codice:" #~ msgid "Or use the link below:" #~ msgstr "Oppure tramite il link sottostante:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "Conferma la tua nuova email tramite il link sottostante:" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Conferma la tua email tramite il link sottostante:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Puoi confermare la tua email tramite il link sottostante:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "Se hai dimenticato la password puoi reimpostarla" #~ msgid " here." #~ msgstr " qui." #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ "Se hai dimenticato la password puoi " #~ "reimpostarla tramite il seguente link:" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Utilizza questo codice per accedere: %(code)s." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ "Se hai dimenticato la password puoi " #~ "reimpostarla qui." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ "Se hai dimenticato la password puoi " #~ "reimpostarla tramite il seguente link: " #~ "%(recovery_link)s" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ "L'operazione WebAuthn deve essere completata" #~ " entro %(within)s. Per favore ricomincia" #~ " da capo." #~ msgid "Nickname for new credential is required." #~ msgstr "Il nome per la nuova credenziale è obbligatorio." #~ msgid "%(name)s is already associated with a credential." #~ msgstr "%(name)s è già associato ad una credenziale." #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "Credenziale WebAuthn con nome: %(name)s eliminata" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "Credenziale WebAuthn con nome: %(name)s aggiunta" #~ msgid "WebAuthn credential id already registered." #~ msgstr "Identificativo di credenziale WebAuthn già registrato." #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "Identificativo di credenziale WebAuthn non registrato." #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "La credenziale WebAuthn non appartiene a nessun utente." #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "Impossibile verificare la credenziale WebAuthn: %(cause)s." #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "Credenziale non registrata per questo uso (primaria o secondaria)" #~ msgid "webauthn" #~ msgstr "webauthn" #~ msgid "WebAuthn Setup" #~ msgstr "Imposta WebAuthn" #~ msgid "Forgot password" #~ msgstr "Password dimenticata" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "Usa WebAuthn per accedere" #~ msgid "Sign in with WebAuthn" #~ msgstr "Accedi con WebAuthn" #~ msgid "WebAuthn" #~ msgstr "WebAuthn" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "Questa applicazione supporta le chiavi di sicurezza WebAuthn." #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ "Utilizzare una chiave di sicurezza " #~ "WebAuthn per eseguire nuovamente " #~ "l'autenticazione" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "Imposta una nuova chiave di sicurezza WebAuthn" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ "Inizia col fornire un nome univoco " #~ "per la tua nuova chiave di " #~ "sicurezza:" #~ msgid "Currently registered security keys:" #~ msgstr "Chiavi di sicurezza attualmente registrate:" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "Elimina la chiave di sicurezza WebAuthn esistente" #~ msgid "WebAuthn Security Key" #~ msgstr "Chiave di sicurezza WebAuthn" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "Accedi utilizzando la chiave di sicurezza WebAuthn" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "Utilizza la chiave di sicurezza WebAuthn come secondo fattore" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "Autenticati nuovamente utilizzando la chiave di sicurezza WebAuthn" flask-security-5.7.1/flask_security/translations/ja_JP/000077500000000000000000000000001511046741400232275ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/ja_JP/LC_MESSAGES/000077500000000000000000000000001511046741400250145ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.po000066400000000000000000001164271511046741400304160ustar00rootroot00000000000000# Japanese translations for Flask-Security. # Copyright (C) 2017 CERN # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2017. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2018-01-25 14:12+0900\n" "Last-Translator: \n" "Language: ja\n" "Language-Team: \n" "Plural-Forms: nplurals=1; plural=0;\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "ログインが必要です" #: flask_security/core.py:297 msgid "Welcome" msgstr "ようこそ" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "メールアドレスの検証" #: flask_security/core.py:299 msgid "Login instructions" msgstr "ログイン手順" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "パスワードが変更されました。" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "パスワード再設定手順" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "アクセス権がありません" #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "ありがとうございます。メールアドレスが検証されました。" #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "メールアドレスは検証済みです" #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "リンクが無効です" #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s のアカウントは既に作成されています" #: flask_security/core.py:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "パスワードが一致しません" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "入力したパスワードが一致していません" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "ドメイン外へのリダイレクトは禁止されています" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "パスワードの再設定手順が %(email)s に送信されました" #: flask_security/core.py:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "リンクが無効です" #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "メールアドレスの検証が必要です" #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "%(email)sにメールアドレス検証手順が再送信されました" #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, 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:486 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "%(email)sにログイン手順が送信されました" #: flask_security/core.py:489 msgid "Invalid login token." msgstr "リンクが無効です" #: flask_security/core.py:490 msgid "Account is disabled." msgstr "アカウントが無効になっています" #: flask_security/core.py:491 msgid "Email not provided" msgstr "メールアドレスを入力してください" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "正しいメールアドレスを入力してください" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "" #: flask_security/core.py:494 msgid "Password not provided" msgstr "パスワードを入力してください" #: flask_security/core.py:496 #, python-format msgid "Password must be at least %(length)s characters" msgstr "" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "入力を確認してください" #: flask_security/core.py:507 msgid "Invalid password" msgstr "入力を確認してください" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "ログインしました" #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "パスワードを忘れた場合" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "パスワードの再設定が完了しました。" #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 msgid "Your new password must be different than your previous password." msgstr "新旧パスワードが同じです" #: flask_security/core.py:529 msgid "You successfully changed your password." msgstr "パスワードが変更されました" #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "ログインしてください" #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "再度ログインしてください" #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:586 msgid "Username not provided" msgstr "" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:64 msgid "Change Method" msgstr "" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "変更" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:67 msgid "Delete" msgstr "" #: flask_security/forms.py:68 msgid "Email Address" msgstr "メールアドレス" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "" #: flask_security/forms.py:71 msgid "Identity" msgstr "" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "ログイン" #: flask_security/forms.py:73 msgid "New Password" msgstr "新しいパスワード" #: flask_security/forms.py:74 msgid "Passcode" msgstr "" #: flask_security/forms.py:75 msgid "Password" msgstr "パスワード" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "再設定手順を送信" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "ユーザ登録" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "次回以降ログインを省略する" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "パスワード変更" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "パスワード再入力" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "検証手順の再送信" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "ログイン手順を送信" #: flask_security/forms.py:85 msgid "Send Code" msgstr "" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:88 msgid "Submit" msgstr "" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "" #: flask_security/forms.py:90 msgid "Username" msgstr "" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:225 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "メニュー" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "メールアドレスの検証" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "パスワード再設定手順の送信" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "パスワード再設定" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "検証手順の再送信" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "パスワードが変更されました。" #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "パスワードを変更した覚えがない場合には、" #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "ようこそ %(email)s !" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:4 msgid "Login now" msgstr "ログイン" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "パスワードを再設定するためにこのリンクを開いてください。" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." 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 "" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ msgid "Please enter your authentication code" #~ msgstr "" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "再度ログインしてください" #~ msgid "Please Enter Your Password" #~ msgstr "" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "パスワードが設定されていません" #~ msgid "Invalid Token" #~ msgstr "" #~ msgid "Your token has been confirmed" #~ msgstr "" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "%(within)s以内にパスワードを設定しませんでした。パスワード再設定手順を %(email)s に再度送信しました。" #~ 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 に送信しました。" #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "ご登録ありがとうございます。%(email)sにメールアドレス検証手順が送信されました。" #~ msgid "Two-factor Login" #~ msgstr "" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "再度ログインしてください" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "パスワードの変更" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "メールアドレスの検証" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "以下のリンクからメールアドレスを検証してください:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "以下のリンクによりメールアドレスを検証できます。" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "パスワードを忘れた場合" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/nl_NL/000077500000000000000000000000001511046741400232465ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/nl_NL/LC_MESSAGES/000077500000000000000000000000001511046741400250335ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.po000066400000000000000000001210031511046741400304170ustar00rootroot00000000000000# Dutch (Netherlands) translations for Flask-Security. # Copyright (C) 2017 CERN # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2017. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2017-05-01 17:52+0200\n" "Last-Translator: FULL NAME \n" "Language: nl_NL\n" "Language-Team: nl_NL \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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Inloggen Verplicht" #: flask_security/core.py:297 msgid "Welcome" msgstr "Welkom" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Gelieve uw e-mailadres te bevestigen" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Aanmeld instructies" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "Uw wachtwoord werd gewijzigd" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Wachtwoord reset instructies" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 #, fuzzy msgid "Verification Code" msgstr "Authenticatie Code" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 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:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Bedankt. Uw e-mailadres werd bevestigd." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "Uw e-mailadres werd reeds bevestigd." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Ongeldige bevestiging token." #: flask_security/core.py:433 #, 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:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "Wachtwoord komt niet overeen" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Wachtwoorden komen niet overeen" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Omleidingen buiten het domein zijn niet toegelaten" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, 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:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Ongeldig wachtwoord reset token." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "E-mailadres moet bevestigd worden." #: flask_security/core.py:471 #, 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:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Je bent niet ingelogd geweest gedurende %(within)s. Nieuwe instructies om" " in te loggen werden verzonden naar%(email)s." #: flask_security/core.py:486 #, 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:489 msgid "Invalid login token." msgstr "Ongeldige aanmelding." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Account is geblokkeerd." #: flask_security/core.py:491 msgid "Email not provided" msgstr "Email niet ingevuld" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Ongeldig e-mailadres" #: flask_security/core.py:493 flask_security/core.py:539 #, fuzzy msgid "Invalid code" msgstr "Niet valide token" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Wachtwoord niet ingevuld" #: flask_security/core.py:496 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" msgstr "Uw wachtwoord moet minstens %(length)s karakters bevatten" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "Deze gebruiker bestaat niet" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Ongeldig wachtwoord" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "U bent succesvol ingelogd." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Wachtwoord vergeten?" #: flask_security/core.py:513 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:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 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:529 msgid "You successfully changed your password." msgstr "Uw wachtwoord werd met succes gewijzigd." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Gelieve in te loggen om deze pagina te zien." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Gelieve opnieuw in te loggen om deze pagina te zien." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "U heeft succesvol uw Dubbele Authenticatie methode veranderd." #: flask_security/core.py:546 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:549 msgid "Marked method is not valid" msgstr "De gemarkeerde methode is niet valide" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 #, fuzzy msgid "Requested method is not valid" msgstr "De gemarkeerde methode is niet valide" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:586 msgid "Username not provided" msgstr "" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:64 msgid "Change Method" msgstr "Verander Methode" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Verander wachtwoord" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "Authenticatie Code" #: flask_security/forms.py:67 msgid "Delete" msgstr "" #: flask_security/forms.py:68 msgid "Email Address" msgstr "E-mailadres" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "" #: flask_security/forms.py:71 msgid "Identity" msgstr "" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Aanmelden" #: flask_security/forms.py:73 msgid "New Password" msgstr "Nieuw wachtwoord" #: flask_security/forms.py:74 #, fuzzy msgid "Passcode" msgstr "wachtwoord" #: flask_security/forms.py:75 msgid "Password" msgstr "wachtwoord" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "Telefoonnummer" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Herstel wachtwoord" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registreer" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Ingelogd blijven" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "reset wachtwoord" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Type wachtwoord opnieuw" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Verzend instructies om te bevestigen opnieuw" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Verzend aanmeld link" #: flask_security/forms.py:85 msgid "Send Code" msgstr "" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:88 msgid "Submit" msgstr "" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "" #: flask_security/forms.py:90 msgid "Username" msgstr "" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "Wachtwoord Verificatie" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:225 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Bevestig account" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Verzend wachtwoord reset instructies" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Reset wachtwoord" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Verzend bevestiging instructies opnieuw" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "The code voor authenticatie is naar uw e-mail adres verzonden" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Uw wachtwoord werd gewijzigd." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Als u uw wachtwoord niet hebt aangepast," #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Welkom, %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Nu inloggen" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Klik hier om uw wachtwoord te resetten" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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 "kan niet in het e-mail account" #: flask_security/templates/security/email/us_instructions.html:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ 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:" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ "Naast uw gebruikersnaam en wachtwoord, " #~ "heeft u ook een code nodig dat " #~ "we u zullen toezenden" #~ msgid "Please enter your authentication code" #~ msgstr "Voer uw authenticatie code in" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "" #~ msgid "Please Enter Your Password" #~ msgstr "Voer uw wachtwoord in" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Er is geen wachtwoord gezet voor deze gebruiker" #~ msgid "Invalid Token" #~ msgstr "Niet valide token" #~ msgid "Your token has been confirmed" #~ msgstr "Uw token is bevestigd" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "U heeft uw wachtwoord niet gereset " #~ "gedurende %(within)s. Nieuwe instructies " #~ "werden verzonden naar %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "U heeft uw e-mailadres niet bevestigd" #~ " in de voorziene %(within)s. Nieuwe " #~ "instructies ter bevestiging van uw " #~ "e-mailadres werden verzonden naar %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "U bent niet aangemeld. Voer alstublieft de juiste gegevens in." #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ 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" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "Naar welk telefoonnummer kunnen we code verzenden?" #~ msgid "enter code" #~ msgstr "" #~ 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" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "Bedankt. Instructies voor bevestiging zijn verzonden naar %(email)s." #~ msgid "Two-factor Login" #~ msgstr "Dubbele Authenticatie Aanmelding" #~ msgid "Two-factor Rescue" #~ msgstr "Dubbele Authenticatie Herstellen" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Gelieve opnieuw in te loggen om deze pagina te zien." #~ msgid "You successfully disabled two factor authorization." #~ msgstr "U heeft succesvol Dubbele Authenticatie uitgeschakeld." #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ 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" #~ msgid "Two factor authentication code" #~ msgstr "Dubbele Authenticatie code" #~ msgid "Two-factor Authentication" #~ msgstr "Dubbele Authenticatie" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Verander wachtwoord" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Bevestig mijn account" #~ msgid "You can log into your account using the following code:" #~ msgstr "U kunt inloggen door de volgende code te gebruiken:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "U kunt inloggen door de volgende code te gebruiken:" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Gelieve uw e-mailadres te bevestigen via onderstaande link:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "U kan uw e-mailadres bevestigen via de onderstaande link:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Wachtwoord vergeten" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/pl_PL/000077500000000000000000000000001511046741400232525ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/pl_PL/LC_MESSAGES/000077500000000000000000000000001511046741400250375ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/pl_PL/LC_MESSAGES/flask_security.po000066400000000000000000001240521511046741400304320ustar00rootroot00000000000000# Polish translation for Flask-Security # 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: 2025-11-03 19:21-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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Logowanie jest wymagane" #: flask_security/core.py:297 msgid "Welcome" msgstr "Witamy" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Prosimy o potwierdzenie Twojego adresu e-mail" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Instrukcje logowania" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "Twoje hasło zostało zmienione" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Instrukcje zmiany hasła" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "Kod weryfikacyjny" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "Nieprawidłowe dane dla żądanego API" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "Nie posiadasz uprawnień, aby wyświetlić tę stronę." #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Dziękujemy. Twój adres e-mail został potwierdzony." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "Twój adres e-mail już został potwierdzony." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Nieprawidłowy token potwierdzania adresu e-mail." #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s jest już powiązany z kontem." #: flask_security/core.py:438 #, 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:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "Hasło nie pasuje" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Hasła nie pasują do siebie" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Przekierowania poza domenę są zabronione" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, 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:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Nieprawidłowy token resetowania hasła." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "Wymagane jest potwierdzenie adresu e-mail." #: flask_security/core.py:471 #, 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:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, 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:486 #, 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:489 msgid "Invalid login token." msgstr "Nieprawidłowy token logowania." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Konto jest wyłączone." #: flask_security/core.py:491 msgid "Email not provided" msgstr "Adres e-mail nie został wprowadzony" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Nieprawidłowy adres e-mail" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "Nieprawidłowy kod" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Hasło nie zostało wprowadzone" #: flask_security/core.py:496 #, 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:499 msgid "Password not complex enough" msgstr "Hasło nie jest wystarczająco złożone" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "Hasło znajduje się na liście haseł wykradzionych" #: flask_security/core.py:502 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:505 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:506 msgid "Specified user does not exist" msgstr "Ten użytkownik nie istnieje" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Nieprawidłowe hasło" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "Hasło lub wprowadzony kod są nieprawidłowe" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Zostałeś zalogowany pomyślnie." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Zapomniałeś hasło?" #: flask_security/core.py:513 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:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 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:529 msgid "You successfully changed your password." msgstr "Pomyślnie zmieniłeś hasło." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Prosimy o zalogowanie się, aby móc odwiedzić tę stronę." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Prosimy o ponowne zalogowanie się, aby móc odwiedzić tę stronę." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "Ponownie zalogowano" #: flask_security/core.py:534 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:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 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:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "Metoda logowania dwuskładnikowego została zmieniona pomyślnie." #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "Nie posiadasz uprawnień, aby odwiedzić tę stronę" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "Wybrana metoda jest niewłaściwa" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, 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:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "Żądana metoda jest niewłaściwa" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "Ujednolicone logowanie przebiegło pomyślnie" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "Musisz ustawić prawidłowy identyfikator, aby się zalogować" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:586 msgid "Username not provided" msgstr "" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 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/forms.py:64 msgid "Change Method" msgstr "Zmień metodę" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Zmień hasło" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "Kod uwierzytelniania" #: flask_security/forms.py:67 msgid "Delete" msgstr "" #: flask_security/forms.py:68 msgid "Email Address" msgstr "Adres e-mail" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "Ustaw przy pomocy adresu e-mail" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "Błędy" #: flask_security/forms.py:71 msgid "Identity" msgstr "Identyfikator" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Zaloguj" #: flask_security/forms.py:73 msgid "New Password" msgstr "Nowe hasło" #: flask_security/forms.py:74 msgid "Passcode" msgstr "Kod dostępu" #: flask_security/forms.py:75 msgid "Password" msgstr "Hasło" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "Numer telefonu" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Odzyskaj hasło" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Zarejestruj" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Zapamiętaj mnie" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Zresetuj hasło" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Przepisz hasło" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Ponownie wyślij instrukcje potwierdzania adresu e-mail" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Wyślij link logowania" #: flask_security/forms.py:85 msgid "Send Code" msgstr "Wyślij kod" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Zaloguj" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "Ustaw przy pomocy wiadomości SMS" #: flask_security/forms.py:88 msgid "Submit" msgstr "Wyślij" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "Kod zatwierdzenia" #: flask_security/forms.py:90 msgid "Username" msgstr "" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "Potwierdź hasło" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "Dostępne metody" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "Kod lub hasło" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "Poprzez adres e-mail" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "Poprzez wiadomość SMS" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:225 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "Logowanie ujednolicone" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Potwierdź konto" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Wyślij instrukcje resetowania hasła" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Resetuj hasło" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Ponownie wyślij instrukcje potwierdzania rejestracji" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 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:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Bezhasłowy kod QR" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Żadna z metod nie została włączona" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: 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:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Kod został wysłany" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Twoje hasło zostało zmienione." #: flask_security/templates/security/email/change_notice.html:4 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:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Witamy %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:4 msgid "Login now" msgstr "Zaloguj teraz" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Kliknij tutaj, aby zresetować swoje hasło" #: flask_security/templates/security/email/reset_instructions.txt:8 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:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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 "brak dostępu do konta mailowego" #: flask_security/templates/security/email/us_instructions.html:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ 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:" #~ msgid "Or use the the link below:" #~ msgstr "Lub używając poniższego linku:" #~ msgid "Username not allowed" #~ msgstr "" #~ 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" #~ msgid "Please enter your authentication code" #~ msgstr "Prosimy o wprowadzenie Twojego kodu uwierzytelniania" #~ msgid "Setup Unified Sign In options" #~ msgstr "Ustaw opcje logowania ujednoliconego" #~ msgid "Please re-authenticate" #~ msgstr "Prosimy o ponowne zalogowanie" #~ msgid "Please Enter Your Password" #~ msgstr "Prosimy o wprowadzenie hasła" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Hasło nie zostało ustawione przez tego użytkownika" #~ msgid "Invalid Token" #~ msgstr "Nieprawidłowy token" #~ msgid "Your token has been confirmed" #~ msgstr "Twój token nie został potwierdzony" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ 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." #~ 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." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ "Nie jesteś zalogowany. Prosimy o " #~ "przesłanie prawidłowych danych uwierzytelniania." #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ 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" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "Na jaki numer telefonu powinien zostać wysłany kod?" #~ msgid "enter code" #~ msgstr "" #~ 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" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ 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." #~ msgid "Two-factor Login" #~ msgstr "Logowanie dwuskładnikowe" #~ msgid "Two-factor Rescue" #~ msgstr "Pomoc w logowaniu dwuskładnikowym" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Musisz zalogować się ponownie, aby wyświetlić tę stronę" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Pomyślnie wyłączyłeś logowanie dwuskładnikowe." #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ 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" #~ msgid "Two factor authentication code" #~ msgstr "Kod uwierzytelniania dwuskładnikowego" #~ msgid "Two-factor Authentication" #~ msgstr "Uwierzytelnianie dwuskładnikowe" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Zmień hasło" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Potwierdź moje konto" #~ 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:" #~ 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:" #~ msgid "Or use the link below:" #~ msgstr "Lub używając poniższego linku:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Prosimy o potwierdzenie Twojego adresu e-mail poprzez poniższy link:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Możesz potwierdzić swój adres e-mail poprzez poniższy link:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Użyj tego kodu, aby się zalogować: %(code)s." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Zapomniałem hasło" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/pt_BR/000077500000000000000000000000001511046741400232525ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/pt_BR/LC_MESSAGES/000077500000000000000000000000001511046741400250375ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.po000066400000000000000000001155441511046741400304400ustar00rootroot00000000000000# Portuguese (Brazil) translations for Flask-Security. # Copyright (C) 2017 CERN # This file is distributed under the same license as the Flask-Security # project. # José Neto , 2017. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2017-09-27 23:39-0300\n" "Last-Translator: José Neto \n" "Language: pt_BR\n" "Language-Team: \n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Login obrigatório" #: flask_security/core.py:297 msgid "Welcome" msgstr "Bem-vindo" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Por favor, confirme seu email" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Instruções de login" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "Sua senha foi alterada" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Instruções para redfinir a senha" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 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:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Obrigado. Seu email foi confirmado." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "Seu email já foi confirmado." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Token de confirmação inválido." #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s já está associado a uma conta." #: flask_security/core.py:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "Senha não confere" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Senhas não conferem" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Redirecionamentos para fora do domínio são proibidos" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, 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:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Token de redefinição de senha inválido." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "O email requer confirmação." #: flask_security/core.py:471 #, 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:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Você não logou dentro de %(within)s. Novas instruções para logar foram " "enviadas para %(email)s." #: flask_security/core.py:486 #, 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:489 msgid "Invalid login token." msgstr "Token de login inválido." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Conta desabilitada." #: flask_security/core.py:491 msgid "Email not provided" msgstr "Email não informado" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Endereço de email inválido" #: flask_security/core.py:493 flask_security/core.py:539 #, fuzzy msgid "Invalid code" msgstr "Senha inválida" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Senha não informada" #: flask_security/core.py:496 #, 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:499 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "Usuário não existe" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Senha inválida" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Você logou com sucesso." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Esqueceu a senha?" #: flask_security/core.py:513 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:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 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:529 msgid "You successfully changed your password." msgstr "Você alterou sua senha com sucesso." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Por favor, logue para acessar esta página." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Por favor, reautentique-se para acessar esta página." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:586 msgid "Username not provided" msgstr "" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:64 msgid "Change Method" msgstr "" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Alterar senha" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:67 msgid "Delete" msgstr "" #: flask_security/forms.py:68 msgid "Email Address" msgstr "Endereço de email" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "" #: flask_security/forms.py:71 msgid "Identity" msgstr "" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Login" #: flask_security/forms.py:73 msgid "New Password" msgstr "Nova senha" #: flask_security/forms.py:74 msgid "Passcode" msgstr "" #: flask_security/forms.py:75 msgid "Password" msgstr "Senha" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Recuperar senha" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registro" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Lembre de mim" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Redefinir senha" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Reescreva a senha" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Reenviar instruções de confirmação" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Enviar link de login" #: flask_security/forms.py:85 msgid "Send Code" msgstr "" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:88 msgid "Submit" msgstr "" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "" #: flask_security/forms.py:90 msgid "Username" msgstr "" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:225 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Confirmar conta" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Enviar instruções para redefinir a senha" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Redefinir senha" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Reenviar instruções de confirmação" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Sua senha foi alterada." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Se você não alterou sua senha," #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Bem-vindo %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Logar agora" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Clique aqui para redefinir sua senha" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." 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 "" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ msgid "Please enter your authentication code" #~ msgstr "" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "Por favor, reautentique-se para acessar esta página." #~ msgid "Please Enter Your Password" #~ msgstr "" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Nenhuma senha definida para este usuário" #~ msgid "Invalid Token" #~ msgstr "" #~ msgid "Your token has been confirmed" #~ msgstr "" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Você não redefiniu sua senha dentro " #~ "de %(within)s. Novas instruções foram " #~ "enviadas para %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Você não confirmou seu email dentro " #~ "de %(within)s. Novas instruções foram " #~ "enviadas para %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ 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." #~ msgid "Two-factor Login" #~ msgstr "" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Por favor, reautentique-se para acessar esta página." #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Alterar senha" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Confirmar minha conta" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Por favor, confirme seu email através do link abaixo:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Você pode confirmar seu email através do link abaixo:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Esqueceu a senha" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/pt_PT/000077500000000000000000000000001511046741400232725ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/pt_PT/LC_MESSAGES/000077500000000000000000000000001511046741400250575ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.po000066400000000000000000001162121511046741400304510ustar00rootroot00000000000000# Portuguese (Portugal) translations for Flask-Security. # Copyright (C) 2017 CERN # This file is distributed under the same license as the Flask-Security # project. # Micael Grilo , 2018. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2018-04-27 14:00+0100\n" "Last-Translator: Micael Grilo \n" "Language: pt_PT\n" "Language-Team: \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Login obrigatório" #: flask_security/core.py:297 msgid "Welcome" msgstr "Bem-vindo" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Por favor, confirme o seu email" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Instruções de login" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "A sua palavra-passe foi alterada" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Instruções para redefinir a palavra-passe" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "Não tem permissões para ver este recurso" #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Obrigado. O seu email foi confirmado." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "O seu email já foi confirmado." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Token de confirmação inválido." #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s já está associado a uma conta." #: flask_security/core.py:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "Palavra-passe não coincide" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Palavras-passe não coincidem" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Redirecionamentos para fora do domínio são proibidos" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, 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:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Token de redefinição de senha inválido." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "O email requer confirmação." #: flask_security/core.py:471 #, 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:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Não iniciou sessão dentro de %(within)s. Novas instruções de inicio de " "sessão foram enviadas para %(email)s." #: flask_security/core.py:486 #, 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:489 msgid "Invalid login token." msgstr "Token de login inválido." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Conta desactivada." #: flask_security/core.py:491 msgid "Email not provided" msgstr "Email em falta" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Endereço de email inválido" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Palavra-passe em falta" #: flask_security/core.py:496 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" msgstr "A palavra-passe deve ter pelo menos %(length)s caracteres" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "Utilizador não existe" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Palavra-passe inválida" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Sessão iniciada com sucesso." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Esqueceu a palavra-passe?" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Redefiniu a sua palavra-passe com sucesso e iniciou sessão " "automaticamente." #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 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:529 msgid "You successfully changed your password." msgstr "Alterou a sua palavra-passe com sucesso." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Por favor, inicie sessão para aceder a esta página." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Por favor, reautentique-se para aceder esta página." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:586 msgid "Username not provided" msgstr "" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:64 msgid "Change Method" msgstr "" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Alterar palavra-passe" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:67 msgid "Delete" msgstr "" #: flask_security/forms.py:68 msgid "Email Address" msgstr "Endereço de email" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "" #: flask_security/forms.py:71 msgid "Identity" msgstr "" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Login" #: flask_security/forms.py:73 msgid "New Password" msgstr "Nova palavra-passe" #: flask_security/forms.py:74 msgid "Passcode" msgstr "" #: flask_security/forms.py:75 msgid "Password" msgstr "Palavra-passe" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Recuperar palavra-passe" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registo" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Lembrar-me" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Redefinir palavra-passe" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Reescreva a palavra-passe" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Reenviar instruções de confirmação" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Enviar endereço de login" #: flask_security/forms.py:85 msgid "Send Code" msgstr "" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:88 msgid "Submit" msgstr "" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "" #: flask_security/forms.py:90 msgid "Username" msgstr "" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:225 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Confirmar conta" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Enviar instruções para redefinir a palavra-passe" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Redefinir palavra-passe" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Reenviar instruções de confirmação" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "A sua palavra-passe foi alterada." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Não alterou a sua palavra-passe," #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Bem-vindo %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Iniciar sessão" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Clique aqui para redefinir a sua palavra-passe" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." 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 "" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ msgid "Please enter your authentication code" #~ msgstr "" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "Por favor, reautentique-se para aceder esta página." #~ msgid "Please Enter Your Password" #~ msgstr "" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Nenhuma palavra-passe foi definida para este utilizador" #~ msgid "Invalid Token" #~ msgstr "" #~ msgid "Your token has been confirmed" #~ msgstr "" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Não redefiniu a sua palavra-passe " #~ "dentro de %(within)s. Novas instruções " #~ "foram enviadas para %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Não confirmou o seu email dentro " #~ "de %(within)s. Novas instruções foram " #~ "enviadas para %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ 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." #~ msgid "Two-factor Login" #~ msgstr "" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Por favor, reautentique-se para aceder esta página." #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Alterar palavra-passe" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Confirmar minha conta" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Por favor, confirme o seu email através do endereço abaixo:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Você pode confirmar o seu email através do endereço abaixo:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Esqueceu a palavra-passe" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/pwl.txt000066400000000000000000000000501511046741400236020ustar00rootroot00000000000000token email thinsp qrcode authenticator flask-security-5.7.1/flask_security/translations/ru_RU/000077500000000000000000000000001511046741400233005ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/ru_RU/LC_MESSAGES/000077500000000000000000000000001511046741400250655ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/ru_RU/LC_MESSAGES/flask_security.po000066400000000000000000001577431511046741400304750ustar00rootroot00000000000000# Russian Translations for Flask-Security. # Copyright (C) 2017 CERN, leovp # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2017. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2024-05-26 04:58+0530\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 && " "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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "Подтвердите свой новый email адрес" #: flask_security/core.py:296 msgid "Login Required" msgstr "Требуется авторизация" #: flask_security/core.py:297 msgid "Welcome" msgstr "Добро пожаловать" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Пожалуйста, подтвердите свой email" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Инструкция для входа" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "Ваш пароль был изменён" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Инструкции для восстановления пароля" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "Код подтверждения" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "Ввод некорректен для запрошенного API" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" "Аутентификация не удалась — идентификатор или пароль/код доступа " "недействительны" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Если этот адрес электронной почты есть в нашей системе, вы получите " "письмо с описанием того, как сбросить пароль." #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "Если этот идентификатор есть в нашей системе, вам был выслан код." #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "У вас нет прав доступа к этому ресурсу." #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "Вы должны авторизоваться, чтобы просмотреть этот ресурс." #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" "Спасибо. Чтобы подтвердить свой email %(email)s, пожалуйста, нажмите на " "ссылку в письме, которое мы только что отправили вам." #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Спасибо. Ваш email был подтверждён." #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "Ваш email уже был подтверждён." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Неверный токен для подтверждения аккаунта." #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s уже привязан к другому аккаунту." #: flask_security/core.py:438 #, 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:444 #, python-format msgid "Identity %(id)s not registered" msgstr "Идентификация %(id)s не зарегистрирована" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" "При взаимодействии с провайдером Oauth произошла ошибка: (%(exerror)s - " "%(exdesc)s). Пожалуйста, попробуйте ещё раз." #: flask_security/core.py:455 msgid "Password does not match" msgstr "Пароль не подходит" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Пароли не совпадают" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Перенаправления вне текущего домена запрещены" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "Код восстановления недействителен" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "Коды восстановления еще не сгенерированы" #: flask_security/core.py:461 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Инструкции по восстановлению пароля были отправлены на %(email)s." #: flask_security/core.py:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "Вы не сбросили пароль в течение %(within)s. " #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Неверный токен для восстановления пароля." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "Email требует подтверждения." #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Инструкции по подтверждению были отправлены на %(email)s." #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "Вы не подтвердили свое письмо в пределах %(within)s. " #: flask_security/core.py:480 #, 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:486 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Инструкции по входу отправлены на %(email)s." #: flask_security/core.py:489 msgid "Invalid login token." msgstr "Неверный токен для входа." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Аккаунт отключён." #: flask_security/core.py:491 msgid "Email not provided" msgstr "Почтовый адрес не введён" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Неверный Email" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "Код недействителен" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Пароль не введён" #: flask_security/core.py:496 #, python-format msgid "Password must be at least %(length)s characters" msgstr "Пароль должен содержать не менее %(length)s символов" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "Пароль недостаточно сложный" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "Пароль в списке скомпрометированных" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "Не удалось соединиться с сайтом скомпрометированных паролей" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "Номер телефона некорректен, например, отсутствует код страны" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "Указанный пользователь не существует" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Неверный пароль" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "Предоставленный пароль или код недействительны" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Вы успешно вошли в систему." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Забыли пароль?" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "Вы успешно сбросили пароль и автоматически вошли в систему." #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" "Вы успешно сбросили свой пароль. Пожалуйста, авторизуйтесь, используя " "новый пароль." #: flask_security/core.py:526 msgid "Your new password must be different than your previous password." msgstr "Ваш новый пароль должен отличаться от предыдущего." #: flask_security/core.py:529 msgid "You successfully changed your password." msgstr "Вы успешно изменили свой пароль." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Пожалуйста, войдите чтобы получить доступ к этой странице." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Пожалуйста, войдите заново, чтобы получить доступ к этой странице." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "Повторный вход выполнен успешно" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "Вы можете получить доступ к данной странице, только если не авторизованы." #: flask_security/core.py:537 msgid "Code has been sent." msgstr "Код отправлен." #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "Не удалось отправить код. Пожалуйста, попробуйте позже" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "Ваш код был подтвержден" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "Вы успешно изменили метод двухфакторной авторизации." #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "В настоящее время у вас нет доступа к данной странице" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "Отмеченный метод недействителен" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" "Настройка должна быть завершена в течение %(within)s. Пожалуйста, начните" " заново." #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "Активные на данный момент варианты входа в систему: %(method_list)s." #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "Запрошенный метод недействителен" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "Настройка единого способа входа прошла успешно" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "Вы должны указать действительный идентификатор для входа" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "Имя пользователя должно содержать не менее %(min)d и не более %(max)d " "символов" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "Имя пользователя содержит недопустимые символы" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "Имя пользователя может содержать только буквы и цифры" #: flask_security/core.py:586 msgid "Username not provided" msgstr "Имя пользователя не указано" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "Имя пользователя %(username)s уже связано с учётной записью." #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s не зарегистрирован с текущим пользователем." #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "Несовпадение учётных данных пользователя" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" "Подтверждение должно быть выполнено в течение %(within)s. Пожалуйста, " "начните сначала." #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "Изменение email адреса подтверждено" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" "Инструкции по подтверждению вашего нового email адреса были отправлены на" " %(email)s." #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" "Настроить с помощью приложения для аутентификации (например google, " "lastpass, authy)" #: flask_security/forms.py:64 msgid "Change Method" msgstr "Изменить метод" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Сменить пароль" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "Код аутентификации" #: flask_security/forms.py:67 msgid "Delete" msgstr "Удалить" #: flask_security/forms.py:68 msgid "Email Address" msgstr "Email адрес" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "Настроить с помощью электронной почты" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "Ошибка(и)" #: flask_security/forms.py:71 msgid "Identity" msgstr "Идентификатор" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Войти" #: flask_security/forms.py:73 msgid "New Password" msgstr "Новый пароль" #: flask_security/forms.py:74 msgid "Passcode" msgstr "Код доступа" #: flask_security/forms.py:75 msgid "Password" msgstr "Пароль" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "Номер телефона" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Восстановить пароль" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Зарегистрироваться" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Запомнить меня" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Сбросить пароль" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Подтверждение пароля" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Отправить повторно инструкции по подтверждению аккаунта" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Отправить ссылку для входа" #: flask_security/forms.py:85 msgid "Send Code" msgstr "Отправить код" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Войти" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "Настроить с помощью СМС" #: flask_security/forms.py:88 msgid "Submit" msgstr "Отправить" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "Отправить код" #: flask_security/forms.py:90 msgid "Username" msgstr "Имя пользователя" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "Подтвердите пароль" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "Google Authenticator" #: flask_security/forms.py:97 msgid "authenticator" msgstr "аутентификатор" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "email" #: flask_security/forms.py:100 msgid "SMS" msgstr "SMS" #: flask_security/forms.py:101 msgid "password" msgstr "пароль" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "ничего" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "Доступные методы" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "Проблемы с доступом к аккаунту?/Утеряно мобильное устройство?" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "Связаться с администратором" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "Показать коды восстановления" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "Генерация новых кодов восстановления" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "Код восстановления" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Доступные методы двухфакторной аутентификации:" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "Выбрать" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "Отправить код по email" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "Использовать ранее загруженный код восстановления" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "Код или пароль" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "По электронной почте" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "По СМС" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "Настроить дополнительный метод входа" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "Удаление активной опции входа" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "Псевдоним" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "Использование" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "Использовать как первичный метод аутентификации" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "Использовать как вторичный метод аутентификации" #: flask_security/webauthn.py:225 msgid "Start" msgstr "Начать" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Меню" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "Выход" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "Изменить зарегистрированный Email" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "Настройка единого входа" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "Единый вход" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Подтвердить аккаунт" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "После отправки, на этот новый email адрес будет отправлено подтверждение." #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "В настоящее время у вас нет пароля — это добавит его." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Отправить инструкции по сбросу пароля" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "или" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "Использовать Social Oauth для входа" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Введите код восстановления" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Коды восстановления" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Обязательно скопируйте их и храните в надёжном месте. Каждый код может " "быть использован только один раз." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "Генерация новых кодов восстановления" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Сбросить пароль" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Заново отправить инструкции по подтверждению" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "Помимо имени пользователя и пароля, вам нужно будет использовать код." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Текущий настроенный двухфакторный метод: %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Откройте приложение аутентификатора на вашем устройстве и просканируйте " "следующий QR-код (или введите код ниже вручную), чтобы начать получать " "коды:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "Введите код, чтобы завершить настройку" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "введите цифровой код" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Это приложение поддерживает настройку кодов восстановления." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Вы можете настроить их здесь." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "Пожалуйста, введите код аутентификации, сгенерированный через: %(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "Код аутентификации был отправлен вам на адрес электронной почты" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "Нам было отправлен email, чтобы сбросить настройки вашей учетной записи" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Настроить единый вход" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Беспарольный QR код" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Никакие методы не были включены — нечего настраивать" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "Введите код, чтобы завершить настройку" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Запросить одноразовый код" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Код отправлен" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "Прозвище: «%s» Использование: «%s» Транспорты: «%s» Обнаруживаемость: " "«%s» Тип устройства: «%s» Резервное копирование «%s» Последнее " "использование: %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "Срок действия этой ссылки истекает через %(within)s." #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "Ваш текущий зарегистрированный email адрес — %(email)s." #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Ваш пароль был изменён." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Если вы не меняли свой пароль," #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Добро пожаловать, %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:4 msgid "Login now" msgstr "Войти" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Нажмите, чтобы сбросить свой пароль" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "Нажмите на ссылку ниже для сброса пароля:" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "Здравствуйте %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Кто-то (вы?) попытался зарегистрировать этот email, который уже есть в " "нашей системе." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" "С этой учетной записью также связано следующее имя пользователя: " "%(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" "Эта учётная запись также имеет следующее имя пользователя, связанное с: " "%(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Вы попытались зарегистрироваться с именем пользователя «%(username)s», " "которое уже связано с другой учётной записью." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "Пожалуйста, повторите процесс регистрации с другим именем пользователя." #~ msgid "" #~ "Open your authenticator app on your " #~ "device and scan the following qrcode " #~ "to start receiving codes:" #~ msgstr "" #~ "Откройте ваше приложение для авторизации " #~ "на вашем устройстве и просканируйте " #~ "данный QR код чтобы начать получать " #~ "коды:" #~ msgid "Or use the the link below:" #~ msgstr "Или используйте данную ссылку:" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ "Помимо вашего имени пользователя и " #~ "пароля, вам нужно будет использовать " #~ "код, который мы вам отправим" #~ msgid "Please enter your authentication code" #~ msgstr "Пожалуйста, введите ваш код аутентификации" #~ msgid "Setup Unified Sign In options" #~ msgstr "Настроить параметры единого входа" #~ msgid "Please re-authenticate" #~ msgstr "Пожалуйста, войдите повторно чтобы получить доступ к этой странице." #~ msgid "Please Enter Your Password" #~ msgstr "Пожалуйста, введите ваш пароль" #~ msgid "No password is set for this user" #~ msgstr "У данного пользователя не установлен пароль" #~ msgid "Invalid Token" #~ msgstr "Токен недействителен" #~ msgid "Your token has been confirmed" #~ msgstr "Ваш токен был подтвержден" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ "Откройте приложение для аутентификации на " #~ "своем устройстве и отсканируйте следующий " #~ "QR-код (или введите код ниже вручную)," #~ " чтобы начать получать пароли:" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ "Псевдоним: \"%s\" Использование: \"%s\" " #~ "Транспорты: \"%s\" Возможность обнаружения: " #~ "\"%s\" Последнее использование: %s" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Вы не восстановили пароль в течение " #~ "%(within)s. Новые инструкции были отправлены" #~ " на %(email)s." #~ 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." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "Вы не аутентифицированы. Пожалуйста, укажите корректные учётные данные." #~ msgid "Currently active sign in options:" #~ msgstr "Активные на данный момент варианты входа в систему:" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ "Для завершения входа введите код, " #~ "отправленный на вашу электронную почту" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "На какой номер телефона необходимо отправить код?" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "Нам было отправлено письмо для сброса вашей учетной записи" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ "Произошла ошибка при взаимодействии с " #~ "провайдером Oauth. Пожалуйста, попробуйте ещё" #~ " раз." #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "Спасибо. Инструкции по подтверждению были отправлены на %(email)s." #~ msgid "Two-factor Login" #~ msgstr "Двухфакторный вход" #~ msgid "Two-factor Rescue" #~ msgstr "Двухфакторное восстановление" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Пожалуйста, войдите повторно чтобы получить доступ к этой странице" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Вы успешно отключили двухфакторную авторизацию." #~ msgid "Disable two factor authentication" #~ msgstr "Отключить двухфакторную аутентификацию" #~ msgid "Two Factor Setup" #~ msgstr "Настройка двухфакторной аутентификации" #~ msgid "Sign in with " #~ msgstr "Войти с помощью " #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "Выбрать метод двухфакторной аутентификации" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ "Двухфакторная аутентификация добавляет " #~ "дополнительный уровень безопасности для вашей" #~ " учётной записи" #~ msgid "Two factor authentication code" #~ msgstr "Код двухфакторной аутентификации" #~ msgid "Two-factor Authentication" #~ msgstr "Двухфакторная аутентификация" #~ msgid "Please Reauthenticate" #~ msgstr "Пожалуйста, повторите аутентификацию" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ "Пожалуйста, повторите аутентификацию, используя " #~ "ключ безопасности WebAuthn" #~ msgid "Change email" #~ msgstr "Изменить email" #~ msgid "Change password" #~ msgstr "Сменить пароль" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "Пожалуйста, подтвердите свой новый email адрес, нажав на ссылку ниже:" #~ msgid "Confirm my new email" #~ msgstr "Подтвердить мой новый email" #~ msgid "Confirm my account" #~ msgstr "Подтвердить аккаунт" #~ msgid "You can log into your account using the following code:" #~ msgstr "Вы можете войти в свою учётную запись с помощью следующего кода:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "Вы можете войти в свою учетную запись с помощью следующего кода:" #~ msgid "Or use the link below:" #~ msgstr "Или используйте ссылку:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "Пожалуйста, подтвердите свой новый email по ссылке ниже:" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Пожалуйста, подтвердите свой email перейдя по ссылке:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Вы можете подтвердить свой почтовый адрес перейдя по ссылке:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "Если вы забыли свой пароль, вы можете восстановить его" #~ msgid " here." #~ msgstr " тут." #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ "Если вы забыли свой пароль, вы " #~ "можете восстановить его по следующей " #~ "ссылке:" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Используйте данный код для входа: %(code)s." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ "Операция WebAuthn должна быть завершена " #~ "в течение %(within)s. Пожалуйста, начните " #~ "сначала." #~ msgid "Nickname for new credential is required." #~ msgstr "Требуется псевдоним для новых учётных данных." #~ msgid "%(name)s is already associated with a credential." #~ msgstr "%(name)s уже связан с учётными данными." #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "Учётные данные WebAuthn с именем %(name)s успешно удалены" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "Учётные данные WebAuthn с именем %(name)s успешно добавлены" #~ msgid "WebAuthn credential id already registered." #~ msgstr "Учётные данные WebAuthn уже зарегистрированы." #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "Незарегистрированный идентификатор учетных данных WebAuthn." #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "Учётные данные WebAuthn не принадлежат ни одному пользователю." #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "Не удалось проверить учётные данные WebAuthn: %(cause)s." #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ "Учётные данные не зарегистрированы для " #~ "этого использования (первичное или вторичное)" #~ msgid "webauthn" #~ msgstr "webauthn" #~ msgid "WebAuthn Setup" #~ msgstr "Настройка WebAuthn" #~ msgid "Forgot password" #~ msgstr "Забыли пароль" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "Использовать WebAuthn для входа" #~ msgid "Sign in with WebAuthn" #~ msgstr "Войти с помощью WebAuthn" #~ msgid "WebAuthn" #~ msgstr "WebAuthn" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "Это приложение поддерживает ключи безопасности WebAuthn." #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "Использовать ключ безопасности WebAuthn для повторной аутентификации" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "Настроить новый ключ WebAuthn" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ "Начните с предоставления уникального имени " #~ "для вашего нового ключа безопасности:" #~ msgid "Currently registered security keys:" #~ msgstr "Зарегистрированные ключи безопасности:" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "Удалить существующий ключ безопасности WebAuthn" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "Войти с помощью ключа безопасности WebAuthn" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ "Использовать ваш ключ безопасности WebAuthn" #~ " как вторичный метод духфакторной " #~ "аутентификации" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/tr_TR/000077500000000000000000000000001511046741400232765ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/tr_TR/LC_MESSAGES/000077500000000000000000000000001511046741400250635ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/tr_TR/LC_MESSAGES/flask_security.po000066400000000000000000001156311511046741400304610ustar00rootroot00000000000000# Turkish translation for Flask-Security. # Copyright (C) 2019 Ecmel B. Canlıer # 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: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2018-12-20 18:48+0300\n" "Last-Translator: Ecmel B. Canlıer \n" "Language: tr_TR\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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "Giriş yapmanız gerekmektedir" #: flask_security/core.py:297 msgid "Welcome" msgstr "Hoş Geldiniz" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "Lütfen e-posta adresinizi onaylayın" #: flask_security/core.py:299 msgid "Login instructions" msgstr "Giriş talimatları" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "Şifreniz değiştirilmiştir" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "Şifre yenileme talimatları" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "Bu maddeyi görmeye yetkiniz yoktur." #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "Teşekkür ederiz. E-posta adresiniz onaylanmıştır" #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "E-posta adresiniz zaten onaylanmış." #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "Yanlış onaylama kodu." #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s başka bir hesaba bağlı." #: flask_security/core.py:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "Şifre yanlış" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "Şifreler uymuyor" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "Adres dışına yönlendirmeler yasaktır" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, 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:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "Yanlış şifre yenileme kodu." #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "E-posta onayı gerekmektedir." #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Onaylama talimatları %(email)s adresine gönderilmiştir." #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "%(within)s içinde giriş yapmadınız. Yeni giriş yapma talimatları " "%(email)s adresine gönderilmiştir." #: flask_security/core.py:486 #, 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:489 msgid "Invalid login token." msgstr "Yanlış giriş kodu." #: flask_security/core.py:490 msgid "Account is disabled." msgstr "Hesap kapalıdır." #: flask_security/core.py:491 msgid "Email not provided" msgstr "E-posta verilmemiş" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "Yanlış e-posta adresi" #: flask_security/core.py:493 flask_security/core.py:539 msgid "Invalid code" msgstr "" #: flask_security/core.py:494 msgid "Password not provided" msgstr "Şifre verilmemiş" #: flask_security/core.py:496 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" msgstr "Şifreniz en az %(length)s karakter olmalıdır" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "Böyle bir kullanıcı yok" #: flask_security/core.py:507 msgid "Invalid password" msgstr "Şifre yanlış" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "Başarıyla giriş yaptınız." #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "Şifrenizi mi unuttunuz?" #: flask_security/core.py:513 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:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 msgid "Your new password must be different than your previous password." msgstr "Yeni şifreniz eski şifrenizden farklı olmalıdır." #: flask_security/core.py:529 msgid "You successfully changed your password." msgstr "Şifrenizi başarıyla değiştirdiniz." #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "Bu sayfaya erişebilmek için lütfen giriş yapın." #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "Bu sayfaya erişebilmek için lütfen tekrardan giriş yapın." #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:586 msgid "Username not provided" msgstr "" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:64 msgid "Change Method" msgstr "" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Şifre Değiştir" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:67 msgid "Delete" msgstr "" #: flask_security/forms.py:68 msgid "Email Address" msgstr "E-posta Adresi" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "" #: flask_security/forms.py:71 msgid "Identity" msgstr "" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Giriş Yap" #: flask_security/forms.py:73 msgid "New Password" msgstr "Yeni Şifre" #: flask_security/forms.py:74 msgid "Passcode" msgstr "" #: flask_security/forms.py:75 msgid "Password" msgstr "Şifre" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "Şifre Kurtar" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Kayıt Ol" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "Beni Hatırla" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "Şifre Yenile" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "Şifre Tekrarı" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "Onaylama Talimatlarını Tekrar Gönder" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "Giriş Linki Gönder" #: flask_security/forms.py:85 msgid "Send Code" msgstr "" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:88 msgid "Submit" msgstr "" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "" #: flask_security/forms.py:90 msgid "Username" msgstr "" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:225 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menü" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "Hesabı onayla" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Şifre değiştirme talimatlarını gönder" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Şifre yenile" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Onaylama talimatlarını tekrar gönder" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Şifreniz değiştirilmiştir." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Eğer siz değiştirmediyseniz," #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Hoş Geldin %(email)s" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Şimdi giriş yap" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "Şifreni yenilemek için buraya tıkla" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." 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 "" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ msgid "Please enter your authentication code" #~ msgstr "" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "Bu sayfaya erişebilmek için lütfen tekrardan giriş yapın." #~ msgid "Please Enter Your Password" #~ msgstr "" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Bu kullanıcı için bir şifre yok" #~ msgid "Invalid Token" #~ msgstr "" #~ msgid "Your token has been confirmed" #~ msgstr "" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Şifrenizi %(within)s içinde yenilemediniz. " #~ "Yeni talimatlar %(email)s adresine " #~ "gönderilmiştir." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "E-posta adresinizi %(within)s içinde " #~ "onaylamadınız. Yeni onaylama talimatları " #~ "%(email)s adresine gönderilmiştir." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "" #~ "Teşekkür ederiz. Onaylama talimatları " #~ "%(email)s adresine gönderilmiştir." #~ msgid "Two-factor Login" #~ msgstr "" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Bu sayfaya erişebilmek için lütfen tekrardan giriş yapın." #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Şifre değiştir" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Hesabımı onayla" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Lütfen e-posta adresinizi aşağıdaki linkten onaylayınız:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "E-posta adresinizi aşağıdaki linkten onaylayabilirsiniz:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Şifremi unuttum" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/translations/zh_Hans_CN/000077500000000000000000000000001511046741400242165ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/zh_Hans_CN/LC_MESSAGES/000077500000000000000000000000001511046741400260035ustar00rootroot00000000000000flask-security-5.7.1/flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.po000066400000000000000000001204101511046741400313700ustar00rootroot00000000000000# Chinese (Simplified, China) translations for Flask-Security. # Copyright (C) 2017 CERN # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2017. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2025-11-03 19:21-0800\n" "PO-Revision-Date: 2018-08-02 19:55+0800\n" "Last-Translator: SteinKuo \n" "Language: zh_CN\n" "Language-Team: Chinese Simplified \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.17.0\n" #: flask_security/core.py:245 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:296 msgid "Login Required" msgstr "需要登录" #: flask_security/core.py:297 msgid "Welcome" msgstr "欢迎" #: flask_security/core.py:298 msgid "Please confirm your email" msgstr "请激活你的电子邮箱" #: flask_security/core.py:299 msgid "Login instructions" msgstr "登录邮件" #: flask_security/core.py:300 #: 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:301 msgid "Your password has been changed" msgstr "你的密码已更改" #: flask_security/core.py:302 msgid "Password reset instructions" msgstr "密码重置" #: flask_security/core.py:303 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:304 msgid "Your requested username" msgstr "" #: flask_security/core.py:307 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:308 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:350 msgid "Verification Code" msgstr "验证码" #: flask_security/core.py:396 msgid "Input not appropriate for requested API" msgstr "输入信息不适合所请求的API" #: flask_security/core.py:398 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:403 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:409 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:412 msgid "You do not have permission to view this resource." msgstr "你无权查看此资源!" #: flask_security/core.py:414 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:418 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:423 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:429 msgid "Thank you. Your email has been confirmed." msgstr "谢谢。你的邮箱已激活!" #: flask_security/core.py:430 msgid "Your email has already been confirmed." msgstr "你的邮箱已激活!" #: flask_security/core.py:431 msgid "Invalid confirmation token." msgstr "无效验证码!" #: flask_security/core.py:433 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s 已关联账户。" #: flask_security/core.py:438 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:444 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:449 #, python-format msgid "" "An error occurred while communicating with the Oauth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:455 msgid "Password does not match" msgstr "密码不匹配" #: flask_security/core.py:456 msgid "Passwords do not match" msgstr "密码不匹配" #: flask_security/core.py:457 msgid "Redirections outside the domain are forbidden" msgstr "禁止域名外重定向" #: flask_security/core.py:458 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:459 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:461 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "重置密码邮件已发送到 %(email)s。" #: flask_security/core.py:465 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:468 msgid "Invalid reset password token." msgstr "密码重置验证码无效!" #: flask_security/core.py:469 msgid "Email requires confirmation." msgstr "请先激活邮箱。" #: flask_security/core.py:471 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "激活邮件已发送到 %(email)s。" #: flask_security/core.py:475 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:480 #, 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:486 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "登录邮件已发送到 %(email)s。" #: flask_security/core.py:489 msgid "Invalid login token." msgstr "无效登录验证码!" #: flask_security/core.py:490 msgid "Account is disabled." msgstr "账户已被禁用!" #: flask_security/core.py:491 msgid "Email not provided" msgstr "未填写电子邮箱" #: flask_security/core.py:492 msgid "Invalid email address" msgstr "无效邮箱地址" #: flask_security/core.py:493 flask_security/core.py:539 #, fuzzy msgid "Invalid code" msgstr "密码不正确" #: flask_security/core.py:494 msgid "Password not provided" msgstr "未填写密码" #: flask_security/core.py:496 #, python-format msgid "Password must be at least %(length)s characters" msgstr "密码至少包含%(length)s个字符" #: flask_security/core.py:499 msgid "Password not complex enough" msgstr "密码不够复杂" #: flask_security/core.py:500 msgid "Password on breached list" msgstr "密码在易被泄露名单上" #: flask_security/core.py:502 msgid "Failed to contact breached passwords site" msgstr "未能连通检测易泄露密码的网站" #: flask_security/core.py:505 msgid "Phone number not valid e.g. missing country code" msgstr "手机号码非法。例如: 缺少国家代码" #: flask_security/core.py:506 msgid "Specified user does not exist" msgstr "此用户不存在" #: flask_security/core.py:507 msgid "Invalid password" msgstr "密码不正确" #: flask_security/core.py:508 msgid "Password or code submitted is not valid" msgstr "提交的密码或代码无效" #: flask_security/core.py:509 msgid "You have successfully logged in." msgstr "你已成功登录!" #: flask_security/core.py:510 flask_security/templates/security/_menu.html:19 #: flask_security/templates/security/_menu.html:65 msgid "Forgot password?" msgstr "忘记密码?" #: flask_security/core.py:513 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "你的密码已成功重置,并已自动登录。" #: flask_security/core.py:520 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:526 msgid "Your new password must be different than your previous password." msgstr "你的新密码不能与当前密码相同。" #: flask_security/core.py:529 msgid "You successfully changed your password." msgstr "你已成功更改密码!" #: flask_security/core.py:530 msgid "Please log in to access this page." msgstr "请登录访问此页面。" #: flask_security/core.py:531 msgid "Please reauthenticate to access this page." msgstr "请重新进行身份验证,以访问此页面。" #: flask_security/core.py:532 msgid "Reauthentication successful" msgstr "成功进行重新认证" #: flask_security/core.py:534 msgid "You can only access this endpoint when not logged in." msgstr "您只能在未登录时访问此端点。" #: flask_security/core.py:537 msgid "Code has been sent." msgstr "" #: flask_security/core.py:538 msgid "Failed to send code. Please try again later" msgstr "发送代码失败。请稍后再试" #: flask_security/core.py:540 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:542 msgid "You successfully changed your two-factor method." msgstr "你成功改变了你的双因素验证方法。" #: flask_security/core.py:546 msgid "You currently do not have permissions to access this page" msgstr "你现在还没有权限访问这个页面" #: flask_security/core.py:549 msgid "Marked method is not valid" msgstr "选择的方法无效" #: flask_security/core.py:551 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:555 flask_security/core.py:564 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "必须在%(within)s内完成设置。请重新开始。" #: flask_security/core.py:559 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:562 msgid "Requested method is not valid" msgstr "非法的请求方法" #: flask_security/core.py:567 msgid "Unified sign in setup successful" msgstr "统一登录设置成功" #: flask_security/core.py:568 msgid "You must specify a valid identity to sign in" msgstr "您必须指定一个有效的身份才能登录" #: flask_security/core.py:569 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:570 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:573 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:579 msgid "Username contains illegal characters" msgstr "" #: flask_security/core.py:583 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:586 msgid "Username not provided" msgstr "" #: flask_security/core.py:588 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:592 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:596 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:600 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:604 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:608 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:612 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:616 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:620 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:624 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:628 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:632 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:636 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:640 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:649 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:655 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:62 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "设置一个认证的app(例如:google、lastpass、authy)" #: flask_security/forms.py:64 msgid "Change Method" msgstr "" #: flask_security/forms.py:65 flask_security/templates/security/_menu.html:14 #: flask_security/templates/security/change_password.html:1 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "更改密码" #: flask_security/forms.py:66 msgid "Authentication Code" msgstr "授权码" #: flask_security/forms.py:67 msgid "Delete" msgstr "" #: flask_security/forms.py:68 msgid "Email Address" msgstr "邮箱地址" #: flask_security/forms.py:69 msgid "Set up using email" msgstr "使用电子邮件进行设置" #: flask_security/forms.py:70 msgid "Error(s)" msgstr "错误" #: flask_security/forms.py:71 msgid "Identity" msgstr "" #: flask_security/forms.py:72 flask_security/templates/security/_menu.html:50 #: flask_security/templates/security/login_user.html:1 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:1 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "登录" #: flask_security/forms.py:73 msgid "New Password" msgstr "新密码" #: flask_security/forms.py:74 msgid "Passcode" msgstr "" #: flask_security/forms.py:75 msgid "Password" msgstr "密码" #: flask_security/forms.py:76 msgid "Phone Number" msgstr "手机号" #: flask_security/forms.py:77 msgid "Recover Password" msgstr "恢复密码" #: flask_security/forms.py:78 flask_security/templates/security/_menu.html:70 msgid "Recover Username" msgstr "" #: flask_security/forms.py:79 flask_security/templates/security/_menu.html:60 #: flask_security/templates/security/register_user.html:1 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "注册" #: flask_security/forms.py:80 msgid "Remember Me" msgstr "记住我" #: flask_security/forms.py:81 msgid "Reset Password" msgstr "重置密码" #: flask_security/forms.py:82 msgid "Retype Password" msgstr "再次确认密码" #: flask_security/forms.py:83 msgid "Resend Confirmation Instructions" msgstr "重新发送邮件验证" #: flask_security/forms.py:84 msgid "Send Login Link" msgstr "发送登录链接" #: flask_security/forms.py:85 msgid "Send Code" msgstr "发送代码" #: flask_security/forms.py:86 #: flask_security/templates/security/us_signin.html:1 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "登录" #: flask_security/forms.py:87 msgid "Set up using SMS" msgstr "用SMS进行设置\"" #: flask_security/forms.py:88 msgid "Submit" msgstr "提交" #: flask_security/forms.py:89 msgid "Submit Code" msgstr "提交代码" #: flask_security/forms.py:90 msgid "Username" msgstr "" #: flask_security/forms.py:91 msgid "Verify Password" msgstr "验证密码" #: flask_security/forms.py:96 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:97 msgid "authenticator" msgstr "" #: flask_security/forms.py:98 flask_security/forms.py:99 msgid "email" msgstr "" #: flask_security/forms.py:100 msgid "SMS" msgstr "" #: flask_security/forms.py:101 msgid "password" msgstr "" #: flask_security/forms.py:102 msgid "passkey" msgstr "" #: flask_security/forms.py:103 msgid "none" msgstr "" #: flask_security/forms.py:957 flask_security/unified_signin.py:167 msgid "Available Methods" msgstr "" #: flask_security/forms.py:959 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1050 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1052 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:142 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:160 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:53 msgid "Select" msgstr "" #: flask_security/twofactor.py:139 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:152 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:160 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:169 msgid "Via email" msgstr "通过邮件" #: flask_security/unified_signin.py:170 msgid "Via SMS" msgstr "通过SMS" #: flask_security/unified_signin.py:301 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:314 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:124 flask_security/webauthn.py:369 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:128 msgid "Usage" msgstr "" #: flask_security/webauthn.py:130 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:225 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "菜单" #: flask_security/templates/security/_menu.html:10 msgid "Sign out" msgstr "" #: flask_security/templates/security/_menu.html:24 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:29 #: flask_security/templates/security/change_username.html:1 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:34 #: flask_security/templates/security/two_factor_setup.html:21 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:39 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:44 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:55 msgid "Unified Sign In" msgstr "统一登陆" #: flask_security/templates/security/_menu.html:75 msgid "Confirm account" msgstr "激活账户" #: flask_security/templates/security/change_email.html:1 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:1 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "发送密码重置邮件" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Oauth to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #, python-format msgid "Sign in with %(provider)s" msgstr "" #: flask_security/templates/security/mf_recovery.html:1 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:1 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:1 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:1 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "重置密码" #: flask_security/templates/security/send_confirmation.html:1 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "重新发送激活邮件" #: flask_security/templates/security/two_factor_select.html:1 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 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:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:1 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "认证代码已发送至您的电子邮件地址。" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:24 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "要求发送一次性代码" #: flask_security/templates/security/us_verify.html:1 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:1 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:9 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "代码已经发送" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:4 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:4 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:8 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:8 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "你的密码已更改。" #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "如果你没更改你的密码," #: flask_security/templates/security/email/change_notice.html:4 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/change_username_notice.html:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:8 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:8 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." 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:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "欢迎你,%(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: 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:4 msgid "Login now" msgstr "立刻登录" #: flask_security/templates/security/email/reset_instructions.html:9 msgid "Click here to reset your password" msgstr "点击这里重置密码" #: flask_security/templates/security/email/reset_instructions.txt:8 msgid "Click the link below to reset your password:" msgstr "" #: 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:9 #: flask_security/templates/security/email/us_instructions.txt:9 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" 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:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:12 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "You can sign in to your account using the following code: %(token)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:12 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." 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 "打开设备上的验证器应用,扫描以下二维码然后接收代码:" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "除了你的用户名和密码外,你还需要使用一个代码,这个代码我们将发送给你" #~ msgid "Please enter your authentication code" #~ msgstr "请输入你的授权码" #~ msgid "Setup Unified Sign In options" #~ msgstr "设置统一登录选项" #~ msgid "Please re-authenticate" #~ msgstr "请重新进行身份验证,以访问此页面。" #~ msgid "Please Enter Your Password" #~ msgstr "请输入你的密码" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "此账户未设置密码" #~ msgid "Invalid Token" #~ msgstr "无效的令牌" #~ msgid "Your token has been confirmed" #~ msgstr "令牌已被确认" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "打开设备上的验证器应用,扫描以下二维码(或手动输入以下代码)开始接收代码:" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "你未在 %(within)s 重置密码。新重置密码邮件已发送到 %(email)s。" #~ 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。" #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "你还没有通过认证。请提供正确的凭证。" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "要完成登录,请输入发送到您邮箱的代码" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "我们应该将代码发送到哪个电话号码?" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "谢谢你。已发送激活邮件到 %(email)s。" #~ msgid "Two-factor Login" #~ msgstr "双因素认证登录" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "请重新进行身份验证,以访问此页面。" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "你成功地禁用了双因素授权。" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "双因素认证为您的账户增加了一层额外的安全保障。" #~ msgid "Two factor authentication code" #~ msgstr "双因素验证代码" #~ msgid "Two-factor Authentication" #~ msgstr "双因素授权" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "更改密码" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "激活账户" #~ msgid "You can log into your account using the following code:" #~ msgstr "您可以使用以下代码登录您的账户:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "您可以使用以下代码登录您的账户。" #~ msgid "Or use the link below:" #~ msgstr "或者使用下面的链接:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "请通过下面链接激活的你的邮箱:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "你可以通过下面链接激活你的邮箱:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "使用此代码登录:%(code)s" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "忘记密码" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" flask-security-5.7.1/flask_security/twofactor.py000066400000000000000000000206671511046741400221200ustar00rootroot00000000000000""" flask_security.two_factor ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security two_factor module :copyright: (c) 2016 by Gal Stainfeld, at Emedgene :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). """ from __future__ import annotations import typing as t from flask import current_app, redirect, request, session from .forms import ( _setup_methods_xlate, get_form_field_xlate, DummyForm, TwoFactorRescueForm, ) from .proxies import _security, _datastore from .tf_plugin import TfPluginBase, tf_clean_session from .utils import ( _, SmsSenderFactory, base_render_json, config_value as cv, do_flash, get_message, localize_callback, json_error_response, send_mail, url_for_security, ) from .signals import ( tf_code_confirmed, tf_disabled, tf_security_token_sent, tf_profile_changed, ) if t.TYPE_CHECKING: # pragma: no cover import flask from flask_security import Security, UserMixin from flask.typing import ResponseValue def tf_send_security_token(user, method, totp_secret, phone_number): """Sends the security token via email/sms for the specified user. :param user: The user to send the code to :param method: The method in which the code will be sent ('email' or 'sms', or 'authenticator') at the moment :param totp_secret: a unique shared secret of the user :param phone_number: If 'sms' phone number to send to There is no return value - it is assumed that exceptions are thrown by underlying methods that callers can catch. Flask-Security code should NOT call this directly - call :meth:`.UserMixin.tf_send_security_token` """ token_to_be_sent = _security.totp_factory.generate_totp_password(totp_secret) if method == "email" or method == "mail": send_mail( cv("EMAIL_SUBJECT_TWO_FACTOR"), user.email, "two_factor_instructions", user=user, token=token_to_be_sent, username=user.calc_username(), ) elif method == "sms": m, c = get_message("USE_CODE", code=token_to_be_sent) from_number = cv("SMS_SERVICE_CONFIG")["PHONE_NUMBER"] to_number = phone_number sms_sender = SmsSenderFactory.createSender(cv("SMS_SERVICE")) sms_sender.send_sms(from_number=from_number, to_number=to_number, msg=m) else: # password are generated automatically in the authenticator apps or not needed token_to_be_sent = None tf_security_token_sent.send( current_app._get_current_object(), _async_wrapper=current_app.ensure_sync, user=user, method=method, token=token_to_be_sent, login_token=token_to_be_sent, phone_number=phone_number, ) def complete_two_factor_process(user, primary_method, totp_secret, is_changing): """clean session according to process (login or changing two-factor method) and perform action accordingly """ _datastore.tf_set(user, primary_method, totp_secret=totp_secret) # if we are changing two-factor method dologin = False if is_changing: # As of 5.5.0 this is the legacy path (using session data) completion_message = "TWO_FACTOR_CHANGE_METHOD_SUCCESSFUL" tf_profile_changed.send( current_app._get_current_object(), _async_wrapper=current_app.ensure_sync, user=user, method=primary_method, ) # if we are logging in for the first time else: completion_message = "TWO_FACTOR_LOGIN_SUCCESSFUL" tf_code_confirmed.send( current_app._get_current_object(), _async_wrapper=current_app.ensure_sync, user=user, method=primary_method, ) dologin = True token = _security.two_factor_plugins.tf_complete(user, dologin) return completion_message, token def set_rescue_options(form: TwoFactorRescueForm, user: UserMixin) -> dict[str, str]: # Based on config - set up options for rescue. # Note that this modifies the passed in Form as well as returns # a dict that can be returned as part of a JSON response. recovery_options = dict(help=url_for_security("two_factor_rescue")) if cv("TWO_FACTOR_RESCUE_EMAIL"): recovery_options["email"] = url_for_security("two_factor_rescue") assert isinstance(form.help_setup.choices, list) form.help_setup.choices.append( ("email", get_form_field_xlate(_("Send code via email"))) ) if ( _security.support_mfa and cv("MULTI_FACTOR_RECOVERY_CODES") and _datastore.mf_get_recovery_codes(user) ): recovery_options["recovery_code"] = url_for_security("mf_recovery") assert isinstance(form.help_setup.choices, list) form.help_setup.choices.append( ( "recovery_code", get_form_field_xlate(_("Use previously downloaded recovery code")), ) ) return recovery_options def tf_disable(user): """Disable two factor for user""" tf_clean_session() _datastore.tf_reset(user) tf_disabled.send( current_app._get_current_object(), _async_wrapper=current_app.ensure_sync, user=user, ) def is_tf_setup(user): """Return True is user account is setup for 2FA.""" return user.tf_totp_secret and user.tf_primary_method class CodeTfPlugin(TfPluginBase): def __init__(self, app: flask.Flask): super().__init__(app) def create_blueprint( self, app: flask.Flask, bp: flask.Blueprint, state: Security ) -> None: pass def get_setup_methods(self, user: UserMixin) -> list[tuple[str, str]]: if is_tf_setup(user): assert user.tf_primary_method is not None return [ ( user.tf_primary_method, localize_callback(_setup_methods_xlate[user.tf_primary_method]), ) ] return [] def tf_login( self, user: UserMixin, json_payload: dict[str, t.Any], next_loc: str | None ) -> ResponseValue: """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. If two-factor is already setup then this sends a code if the method requires it. If not, then user is redirected to two-factor-setup. In either case we do NOT log in user, so we must store some info in session to track our state (including what user). """ # if user's two-factor properties are not configured if not is_tf_setup(user): session["tf_state"] = "setup_from_login" json_payload["tf_state"] = "setup_from_login" if not _security._want_json(request): return redirect(url_for_security("two_factor_setup")) # if user's two-factor properties are configured else: session["tf_state"] = "ready" json_payload["tf_state"] = "ready" json_payload["tf_primary_method"] = user.tf_primary_method json_payload["tf_method"] = user.tf_primary_method if user.tf_primary_method in ["mail", "email", "sms"]: msg = user.tf_send_security_token( method=user.tf_primary_method, totp_secret=user.tf_totp_secret, phone_number=getattr(user, "tf_phone_number", None), ) if msg: # send code didn't work if not _security._want_json(request): # This is a mess - # we are deep down in the login/unified sign in flow. do_flash(msg, "error") return redirect(url_for_security("login")) else: payload = json_error_response(errors=msg) return _security._render_json(payload, 500, None, None) if not _security._want_json(request): values = dict(next=next_loc) if next_loc else dict() return redirect( url_for_security("two_factor_token_validation", **values) ) # JSON response - Fake up a form - doesn't really matter which. form = DummyForm(formdata=None) return base_render_json(form, include_user=False, additional=json_payload) flask-security-5.7.1/flask_security/unified_signin.py000066400000000000000000001131101511046741400230640ustar00rootroot00000000000000""" flask_security.unified_signin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security Unified Signin module :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. This implements a unified sign in endpoint - allowing authentication via identity and passcode - where identity is configured via SECURITY_USER_IDENTITY_ATTRIBUTES, and allowable passcodes are one of US_ENABLED_METHODS. 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. Consider/Questions: - Allow registering/confirming with just a phone number - this likely would require a new register/confirm endpoint in order to implement verification. - Right now ChangePassword won't work - it requires an existing password - so if the user doesn't have one - can't change it. However ForgotPassword will in fact allow the user to add a password. Is that sufficient? - This also means that there is no way to REMOVE your password once it is setup, although user can register without one. - separate code validation times for SMS, email, authenticator? - token versus code versus passcode? Confusing terminology. """ from __future__ import annotations import time import typing as t from flask import current_app from flask import after_this_request, request, session from flask_login import current_user from wtforms import ( BooleanField, PasswordField, RadioField, SelectMultipleField, StringField, SubmitField, TelField, validators, ) from wtforms.widgets import CheckboxInput from .confirmable import requires_confirmation from .decorators import anonymous_user_required, auth_required, unauth_csrf from .forms import ( _setup_methods_xlate, Form, NextFormMixin, RequiredLocalize, build_form_from_request, build_form, form_errors_munge, generic_message, get_form_field_label, get_form_field_xlate, ) from .proxies import _security, _datastore from .quart_compat import get_quart_status from .signals import us_profile_changed, us_security_token_sent from .utils import ( _, SmsSenderFactory, base_render_json, check_and_get_token_status, config_value as cv, do_flash, get_identity_attributes, get_post_login_redirect, get_post_verify_redirect, get_message, get_url, get_within_delta, handle_already_auth, is_user_authenticated, localize_callback, login_user, lookup_identity, propagate_next, send_mail, url_for_security, view_commit, ) from .twofactor import tf_clean_session from .webauthn import has_webauthn if t.TYPE_CHECKING: # pragma: no cover from flask.typing import ResponseValue from flask_security import UserMixin if get_quart_status(): # pragma: no cover from quart import redirect else: from flask import redirect def _compute_code_methods(): # Return list of methods that actually send codes return list(set(cv("US_ENABLED_METHODS")) - {"password", "authenticator"}) def _compute_setup_methods(): # Return list of methods that require setup return list(set(cv("US_ENABLED_METHODS")) - {"password"}) def _compute_active_methods(user): # Compute methods already setup. active_methods = set(cv("US_ENABLED_METHODS")) & set( _datastore.us_get_totp_secrets(user).keys() ) if user.password: active_methods = active_methods.union({"password"}) return list(active_methods) def _compute_active_code_methods(user): return list(set(_compute_active_methods(user)) & set(_compute_code_methods())) def _us_common_validate(form): # Be aware - this has side effect on the form - it will fill in # the form.user # 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? form.user = lookup_identity(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.""" # filled in by caller user: UserMixin # Filled in here authn_via: str # PasswordField so it doesn't show, no autocomplete since it might be a password # but it might be a passcode. passcode = PasswordField( get_form_field_label("passcode"), render_kw={ "placeholder": get_form_field_xlate(_("Code or Password")), "autocomplete": "off", }, ) submit = SubmitField(get_form_field_label("submit")) chosen_method = RadioField( _("Available Methods"), choices=[ ("email", get_form_field_xlate(_("Via email"))), ("sms", get_form_field_xlate(_("Via SMS"))), ], validators=[validators.Optional()], ) submit_send_code = SubmitField(get_form_field_label("sendcode")) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False return True def validate2(self) -> bool: totp_secrets = _datastore.us_get_totp_secrets(self.user) if self.submit.data: # This is authn - verify passcode/password # Since we have a unique totp_secret for each method - we # can figure out which mechanism was used. # Note that password check requires a string (not int or None) assert isinstance(self.passcode.errors, list) passcode = self.passcode.data if not passcode: self.passcode.errors.append(get_message("INVALID_PASSWORD_CODE")[0]) return False passcode = str(passcode) ok = False for method in cv("US_ENABLED_METHODS"): if method == "password" and self.user.password: passcode = _security.password_util.normalize(passcode) if self.user.verify_and_update_password(passcode): ok = True break else: if method in totp_secrets and _security.totp_factory.verify_totp( token=passcode, totp_secret=totp_secrets[method], user=self.user, window=cv("US_TOKEN_VALIDITY"), ): ok = True break if not ok: self.passcode.errors.append(get_message("INVALID_PASSWORD_CODE")[0]) return False self.authn_via = method return True elif self.submit_send_code.data: # Send a code - chosen_method must be valid assert isinstance(self.chosen_method.errors, list) cm = self.chosen_method.data if cm not in cv("US_ENABLED_METHODS"): self.chosen_method.errors.append( get_message("US_METHOD_NOT_AVAILABLE")[0] ) return False if cm not in totp_secrets: self.chosen_method.errors.append( get_message("US_METHOD_NOT_AVAILABLE")[0] ) return False if cm == "sms" and not self.user.us_phone_number: # They need to us-setup! self.chosen_method.errors.append(get_message("PHONE_INVALID")[0]) return False return True return False # pragma: no cover class UnifiedSigninForm(_UnifiedPassCodeForm, NextFormMixin): """A unified login form For either identity/password or request and enter code. """ identity = StringField( get_form_field_label("identity"), validators=[RequiredLocalize()], ) remember = BooleanField(get_form_field_label("remember_me")) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.remember.default = cv("DEFAULT_REMEMBER_ME") self.requires_confirmation = False def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False if not _us_common_validate(self): return False if not super().validate2(): return False # Can't authenticate nor get a code if still required confirmation. self.requires_confirmation = requires_confirmation(self.user) assert isinstance(self.identity.errors, list) if self.requires_confirmation: self.identity.errors.append(get_message("CONFIRMATION_REQUIRED")[0]) return False return True class UnifiedVerifyForm(_UnifiedPassCodeForm): """Verify authentication. This is for freshness 'reauthentication' required. """ def validate(self, **kwargs: t.Any) -> bool: self.user = current_user if not super().validate(**kwargs): return False if not super().validate2(): return False return True class UnifiedSigninSetupForm(Form): """Setup form""" setup_choices = [ ("email", get_form_field_label("email_method")), ( "authenticator", get_form_field_label("authapp_method"), ), ("sms", get_form_field_label("sms_method")), ] chosen_method = RadioField( get_form_field_xlate(_("Setup additional sign in option")), validate_choice=False, ) delete_choices = [ ("email", get_form_field_xlate("Delete email option")), ( "authenticator", get_form_field_xlate("Delete authenticator option"), ), ("sms", get_form_field_xlate("Delete SMS option")), ] delete_method = SelectMultipleField( get_form_field_xlate(_("Delete active sign in option")), option_widget=CheckboxInput(), validate_choice=False, ) phone = TelField(get_form_field_label("phone")) submit = SubmitField(get_form_field_label("submit")) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False assert isinstance(self.chosen_method.errors, list) assert isinstance(self.phone.errors, list) assert isinstance(self.delete_method.errors, list) if not self.chosen_method.data and not self.delete_method.data: self.form_errors.append(get_message("API_ERROR")[0]) return False if self.chosen_method.data: if self.chosen_method.data not in cv("US_ENABLED_METHODS"): self.chosen_method.errors.append( get_message("US_METHOD_NOT_AVAILABLE")[0] ) return False if self.chosen_method.data == "sms": if not self.phone.data: self.phone.errors.append(get_message("PHONE_INVALID")[0]) return False msg = _security.phone_util.validate_phone_number(self.phone.data) if msg: self.phone.errors.append(msg) return False # As an identity attribute - it MUST be unique! cphone = _security.phone_util.get_canonical_form(self.phone.data) if _datastore.find_user(us_phone_number=cphone): msg = get_message( "IDENTITY_ALREADY_ASSOCIATED", attr="us_phone_number", value=cphone, )[0] self.phone.errors.append(msg) return False if self.delete_method.data: if not all( m in _compute_active_methods(current_user) for m in self.delete_method.data ): self.delete_method.errors.append( get_message("US_METHOD_NOT_AVAILABLE")[0] ) return False return True class UnifiedSigninSetupValidateForm(Form): """The unified sign in setup validation form""" # These 2 filled in by view user: UserMixin totp_secret: str passcode = StringField( get_form_field_label("passcode"), render_kw={ "autocomplete": "one-time-code", "type": "text", "pattern": "[0-9]*", }, validators=[RequiredLocalize()], ) submit = SubmitField(get_form_field_label("submitcode"), id="submit-code") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False assert isinstance(self.passcode.errors, list) assert self.passcode.data is not None # RequiredLocalize validator if not _security.totp_factory.verify_totp( token=self.passcode.data, totp_secret=self.totp_secret, user=self.user, window=cv("US_TOKEN_VALIDITY"), ): self.passcode.errors.append(get_message("INVALID_PASSWORD_CODE")[0]) return False return True def _send_code_helper(form, send_magic_link): # send code user = form.user method = form.chosen_method.data totp_secrets = _datastore.us_get_totp_secrets(user) msg = user.us_send_security_token( method, totp_secret=totp_secrets[method], phone_number=getattr(user, "us_phone_number", None), send_magic_link=send_magic_link, ) return msg @anonymous_user_required @unauth_csrf() def us_signin_send_code() -> ResponseValue: """ Send code view. POST only. This takes an identity (as configured in USER_IDENTITY_ATTRIBUTES) and a method request to send a code. """ form = t.cast(UnifiedSigninForm, build_form_from_request("us_signin_form")) form.submit_send_code.data = True form.submit.data = False code_methods = _compute_code_methods() if form.validate_on_submit(): msg = _send_code_helper(form, True) if msg: assert isinstance(form.chosen_method.errors, list) form.chosen_method.errors.append(msg) if _security._want_json(request): # Not authenticated yet - so don't send any user info. return base_render_json( form, include_user=False, error_status_code=500 if msg else 200 ) # Make sure same response as non-setup method below do_flash(*generic_message("CODE_HAS_BEEN_SENT", "GENERIC_US_SIGNIN")) return _security.render_template( cv("US_SIGNIN_TEMPLATE"), us_signin_form=form, available_methods=cv("US_ENABLED_METHODS"), code_methods=code_methods, chosen_method=form.chosen_method.data, skip_loginmenu=True, **_security._run_ctx_processor("us_signin"), ) elif request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): # TODO - this suppresses the error if they don't select ANY send code option. rinfo: dict[str, dict[str, str]] = dict( identity=dict(), passcode=dict(), chosen_method=dict() ) form_errors_munge(form, rinfo) if not _security._want_json(request): # Make sure same response as successful code above. do_flash(*get_message("GENERIC_US_SIGNIN")) # Here on failed validation if _security._want_json(request): payload = { "available_methods": cv("US_ENABLED_METHODS"), "code_methods": code_methods, "identity_attributes": get_identity_attributes(), } return base_render_json(form, include_user=False, additional=payload) if ( form.requires_confirmation and cv("REQUIRES_CONFIRMATION_ERROR_VIEW") and not cv("RETURN_GENERIC_RESPONSES") ): do_flash(*get_message("CONFIRMATION_REQUIRED")) return redirect(get_url(cv("REQUIRES_CONFIRMATION_ERROR_VIEW"))) return _security.render_template( cv("US_SIGNIN_TEMPLATE"), us_signin_form=form, available_methods=cv("US_ENABLED_METHODS"), code_methods=code_methods, skip_loginmenu=True, **_security._run_ctx_processor("us_signin"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def us_verify_send_code() -> ResponseValue: """ Send code during verify. POST only. """ form = t.cast(UnifiedVerifyForm, build_form_from_request("us_verify_form")) form.submit_send_code.data = True form.submit.data = False code_methods = _compute_active_code_methods(current_user) if form.validate_on_submit(): msg = _send_code_helper(form, False) if msg: assert isinstance(form.chosen_method.errors, list) form.chosen_method.errors.append(msg) if _security._want_json(request): # no real reason to send user info? return base_render_json( form, include_user=False, error_status_code=500 if msg else 200 ) if not msg: do_flash(*get_message("CODE_HAS_BEEN_SENT")) return _security.render_template( cv("US_VERIFY_TEMPLATE"), us_verify_form=form, available_methods=cv("US_ENABLED_METHODS"), code_methods=code_methods, chosen_method=form.chosen_method.data, skip_login_menu=True, **_security._run_ctx_processor("us_verify"), ) # Here on failed validation if _security._want_json(request): return base_render_json(form) return _security.render_template( cv("US_VERIFY_TEMPLATE"), us_verify_form=form, available_methods=cv("US_ENABLED_METHODS"), code_methods=code_methods, skip_login_menu=True, **_security._run_ctx_processor("us_verify"), ) @unauth_csrf() def us_signin() -> ResponseValue: """ Unified sign in view. This takes an identity (as configured in USER_IDENTITY_ATTRIBUTES) and a passcode (password or OTP). """ form = t.cast(UnifiedSigninForm, build_form_from_request("us_signin_form")) form.submit.data = True form.submit_send_code.data = False code_methods = _compute_code_methods() payload = { "available_methods": cv("US_ENABLED_METHODS"), "code_methods": code_methods, "identity_attributes": get_identity_attributes(), } if is_user_authenticated(current_user): return handle_already_auth(form, payload=payload) # Clean out any potential old session info - in case of previous # aborted 2FA attempt. tf_clean_session() if form.validate_on_submit(): # Check if multi-factor is required. Some (this is configurable) don't # need 2FA since they ARE multi-factor (such as SMS and authenticator). remember_me = form.remember.data if "remember" in form else None if form.authn_via in cv("US_MFA_REQUIRED"): response = _security.two_factor_plugins.tf_enter( form.user, remember_me, form.authn_via, next_loc=propagate_next(request.url, form), ) if response: return response after_this_request(view_commit) login_user(form.user, remember=remember_me, authn_via=[form.authn_via]) if _security._want_json(request): return base_render_json(form, include_auth_token=True) return redirect(get_post_login_redirect()) # Here on GET or failed POST validate if request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): rinfo = dict( identity=dict(replace_msg="GENERIC_AUTHN_FAILED"), passcode=dict(replace_msg="GENERIC_AUTHN_FAILED"), ) form_errors_munge(form, rinfo) if request.method == "GET": # set CSRF COOKIE if configured. This is the equivalent of forms and # base_render_json always sending the csrf_token session["fs_cc"] = "set" if _security._want_json(request): return base_render_json(form, include_user=False, additional=payload) # On error - wipe code form.passcode.data = None if ( form.requires_confirmation and cv("REQUIRES_CONFIRMATION_ERROR_VIEW") and not cv("RETURN_GENERIC_RESPONSES") ): do_flash(*get_message("CONFIRMATION_REQUIRED")) return redirect(get_url(cv("REQUIRES_CONFIRMATION_ERROR_VIEW"))) return _security.render_template( cv("US_SIGNIN_TEMPLATE"), us_signin_form=form, available_methods=cv("US_ENABLED_METHODS"), code_methods=code_methods, skip_login_menu=True, **_security._run_ctx_processor("us_signin"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def us_verify() -> ResponseValue: """ Re-authenticate to reset freshness time. This is likely the result of a reauthn_handler redirect, which will have filled in ?next=xxx - which we want to carefully not lose as we go through these steps. """ form = t.cast(UnifiedVerifyForm, build_form_from_request("us_verify_form")) form.submit.data = True form.submit_send_code.data = False code_methods = _compute_active_code_methods(current_user) if form.validate_on_submit(): # verified - so set freshness time. session["fs_paa"] = time.time() if _security._want_json(request): return base_render_json(form, include_auth_token=True) do_flash(*get_message("REAUTHENTICATION_SUCCESSFUL")) return redirect(get_post_verify_redirect()) # Here on GET or failed POST validate webauthn_available = has_webauthn(current_user, cv("WAN_ALLOW_AS_VERIFY")) if _security._want_json(request): payload = { "available_methods": cv("US_ENABLED_METHODS"), "code_methods": code_methods, "has_webauthn_verify_credential": webauthn_available, } return base_render_json(form, additional=payload) # On error - wipe code form.passcode.data = None return _security.render_template( cv("US_VERIFY_TEMPLATE"), us_verify_form=form, code_methods=code_methods, skip_login_menu=True, has_webauthn_verify_credential=webauthn_available, wan_verify_form=build_form("wan_verify_form"), **_security._run_ctx_processor("us_verify"), ) @anonymous_user_required def us_verify_link() -> ResponseValue: """ Used to verify a magic email link. GET only Since this is just a URL - be careful not to disclose info like whether email exists or not. """ fs_uniquifier = request.args.get("id", None) code = request.args.get("code", None) if not fs_uniquifier or not code: m, c = get_message("API_ERROR") if cv("REDIRECT_BEHAVIOR") == "spa": return redirect(get_url(cv("LOGIN_ERROR_VIEW"), qparams={c: m})) do_flash(m, c) return redirect(url_for_security("us_signin")) user = _datastore.find_user(fs_uniquifier=fs_uniquifier) if not user or not user.active: if not user: m, c = generic_message("USER_DOES_NOT_EXIST", "GENERIC_AUTHN_FAILED") else: m, c = generic_message("DISABLED_ACCOUNT", "GENERIC_AUTHN_FAILED") if cv("REDIRECT_BEHAVIOR") == "spa": return redirect(get_url(cv("LOGIN_ERROR_VIEW"), qparams={c: m})) do_flash(m, c) return redirect(url_for_security("us_signin")) totp_secrets = _datastore.us_get_totp_secrets(user) if "email" not in totp_secrets or not _security.totp_factory.verify_totp( token=code, totp_secret=totp_secrets["email"], user=user, window=cv("US_TOKEN_VALIDITY"), ): m, c = generic_message("INVALID_CODE", "GENERIC_AUTHN_FAILED") if cv("REDIRECT_BEHAVIOR") == "spa": return redirect( get_url( cv("LOGIN_ERROR_VIEW"), qparams=user.get_redirect_qparams({c: m}), ) ) do_flash(m, c) return redirect(url_for_security("us_signin")) tf_setup_methods = [] if cv("TWO_FACTOR"): tf_setup_methods = _security.two_factor_plugins.get_setup_tf_methods(user) if ( cv("TWO_FACTOR") and "email" in cv("US_MFA_REQUIRED") and (cv("TWO_FACTOR_REQUIRED") or len(tf_setup_methods) > 0) ): # tf_login doesn't know anything about "spa" etc. In general two-factor # isn't quite ready for SPA. So we return an error via a redirect rather # than mess up SPA applications. To be clear - this simply doesn't # work - using a magic link w/ 2FA - need to use code. if cv("REDIRECT_BEHAVIOR") == "spa": return redirect( get_url( cv("LOGIN_ERROR_VIEW"), qparams=user.get_redirect_qparams({"tf_required": 1}), ) ) response = _security.two_factor_plugins.tf_enter( user, False, "email", next_loc=propagate_next(request.url, None) ) if response: return response login_user(user, authn_via=["email"]) after_this_request(view_commit) if cv("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 # possibly HTTP Referer header). # This means that this can only work if sessions are active which sort of # makes sense - otherwise you need to use /us-signin with a code. return redirect( get_url(cv("POST_LOGIN_VIEW"), qparams=user.get_redirect_qparams()) ) do_flash(*get_message("PASSWORDLESS_LOGIN_SUCCESSFUL")) return redirect(get_post_login_redirect()) @auth_required( lambda: cv("API_ENABLED_METHODS"), within=lambda: cv("FRESHNESS"), grace=lambda: cv("FRESHNESS_GRACE_PERIOD"), ) def us_setup() -> ResponseValue: """ Change unified sign in methods. We want to verify the new method - so don't store anything yet in DB use a timed signed token to pass along state. GET - retrieve current info (json) or form. """ form = t.cast(UnifiedSigninSetupForm, build_form_from_request("us_setup_form")) setup_methods = _compute_setup_methods() active_methods = _compute_active_methods(current_user) form.chosen_method.choices = [ c for c in form.setup_choices if c[0] not in active_methods and c[0] in setup_methods ] form.delete_method.choices = [ c for c in form.delete_choices if c[0] in active_methods ] # translate active methods if not active_methods: active_methods = [None] current_methods = _security.i18n_domain.format_list( [localize_callback(_setup_methods_xlate[m]) for m in active_methods] ) current_methods_msg = get_message( "US_CURRENT_METHODS", method_list=current_methods )[0] if form.validate_on_submit(): qrcode_values = dict() json_response = dict() state_token = None delete_method = form.delete_method.data add_method = form.chosen_method.data if delete_method: after_this_request(view_commit) for m in delete_method: _datastore.us_reset(current_user, m) active_methods = _compute_active_methods(current_user) if not active_methods: active_methods = [None] current_methods = _security.i18n_domain.format_list( [localize_callback(_setup_methods_xlate[m]) for m in active_methods] ) current_methods_msg = get_message( "US_CURRENT_METHODS", method_list=current_methods )[0] form.chosen_method.choices = [ c for c in form.setup_choices if c[0] not in active_methods and c[0] in setup_methods ] form.delete_method.choices = [ c for c in form.delete_choices if c[0] in active_methods ] form.delete_method.data = None us_profile_changed.send( current_app._get_current_object(), # type: ignore _async_wrapper=current_app.ensure_sync, # type: ignore[arg-type] user=current_user, methods=delete_method, delete=True, ) if add_method: # Always generate a totp_secret. We don't set it in the DB until # user has successfully validated. totp = _security.totp_factory.generate_totp_secret() # 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 = None if add_method == "sms": assert form.phone.data is not None phone_number = _security.phone_util.get_canonical_form(form.phone.data) state = { "totp_secret": totp, "chosen_method": add_method, "phone_number": phone_number, } msg = current_user.us_send_security_token( method=add_method, totp_secret=totp, phone_number=phone_number, ) if msg: # sending didn't work. assert isinstance(form.chosen_method.errors, list) form.chosen_method.errors.append(msg) if _security._want_json(request): # Not authenticated yet - so don't send any user info. return base_render_json( form, include_user=False, error_status_code=500 if msg else 400 ) return _security.render_template( cv("US_SETUP_TEMPLATE"), available_methods=cv("US_ENABLED_METHODS"), active_methods=active_methods, current_methods_msg=current_methods_msg, setup_methods=setup_methods, us_setup_form=form, **_security._run_ctx_processor("us_setup"), ) state_token = _security.us_setup_serializer.dumps(state) json_response = dict( chosen_method=form.chosen_method.data, phone=phone_number, state=state_token, ) 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): return base_render_json(form, include_user=False, additional=json_response) form.delete_method.data = None return _security.render_template( cv("US_SETUP_TEMPLATE"), available_methods=cv("US_ENABLED_METHODS"), active_methods=active_methods, current_methods_msg=current_methods_msg, setup_methods=setup_methods, code_sent=form.chosen_method.data in _compute_code_methods(), chosen_method=form.chosen_method.data, us_setup_form=form, us_setup_validate_form=build_form("us_setup_validate_form"), **qrcode_values, state=state_token, **_security._run_ctx_processor("us_setup"), ) phone_number = None if "sms" in cv("US_ENABLED_METHODS"): phone_number = current_user.us_phone_number # Get here on initial new setup (GET) # Or failure of POST if _security._want_json(request): payload = { "identity_attributes": get_identity_attributes(), "available_methods": cv("US_ENABLED_METHODS"), "active_methods": active_methods, "setup_methods": setup_methods, "phone": phone_number, } return base_render_json(form, include_user=False, additional=payload) # Show user existing phone number form.phone.data = phone_number form.chosen_method.data = None form.delete_method.data = None return _security.render_template( cv("US_SETUP_TEMPLATE"), available_methods=cv("US_ENABLED_METHODS"), active_methods=active_methods, current_methods_msg=current_methods_msg, setup_methods=setup_methods, us_setup_form=form, **_security._run_ctx_processor("us_setup"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def us_setup_validate(token: str) -> ResponseValue: """ Validate new setup. The token is the state variable which is signed and timed and contains all the state that once confirmed will be stored in the user record. """ form = t.cast( UnifiedSigninSetupValidateForm, build_form_from_request("us_setup_validate_form"), ) expired, invalid, state = check_and_get_token_status( token, "us_setup", get_within_delta("US_SETUP_WITHIN") ) if invalid: m, c = get_message("API_ERROR") if expired: m, c = get_message("US_SETUP_EXPIRED", within=cv("US_SETUP_WITHIN")) if invalid or expired: if _security._want_json(request): form.form_errors.append(m) return base_render_json(form, include_user=False) do_flash(m, c) return redirect(url_for_security("us_setup")) form.totp_secret = state["totp_secret"] form.user = current_user if form.validate_on_submit(): 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) us_profile_changed.send( current_app._get_current_object(), # type: ignore _async_wrapper=current_app.ensure_sync, # type: ignore[arg-type] user=current_user, methods=[method], delete=False, ) if _security._want_json(request): phone_number = None if "sms" in cv("US_ENABLED_METHODS"): phone_number = current_user.us_phone_number return base_render_json( form, include_user=False, additional=dict(chosen_method=method, phone=phone_number), ) else: do_flash(*get_message("US_SETUP_SUCCESSFUL")) return redirect(get_url(cv("US_POST_SETUP_VIEW"))) # Code not correct/outdated. if _security._want_json(request): return base_render_json(form, include_user=False) m, c = get_message("INVALID_PASSWORD_CODE") do_flash(m, c) return redirect(url_for_security("us_setup")) def us_send_security_token( user, method, totp_secret, phone_number, send_magic_link=False ): """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 :param totp_secret: the unique shared secret of the user :param phone_number: If 'sms' phone number to send to :param send_magic_link: If true a magic link that can be clicked on will be sent. This shouldn't be sent during a setup. There is no return value - it is assumed that exceptions are thrown by underlying methods that callers can catch. Flask-Security code should NOT call this directly - call :meth:`.UserMixin.us_send_security_token` .. versionadded:: 3.4.0 """ code = _security.totp_factory.generate_totp_password(totp_secret) if method == "email": login_link = None if send_magic_link: login_link = url_for_security( "us_verify_link", id=str(user.fs_uniquifier), code=code, _external=True ) send_mail( cv("US_EMAIL_SUBJECT"), user.email, "us_instructions", user=user, username=user.calc_username(), token=code, # deprecated login_token=code, login_link=login_link, ) elif method == "sms": m, c = get_message("USE_CODE", code=code) from_number = cv("SMS_SERVICE_CONFIG")["PHONE_NUMBER"] to_number = phone_number sms_sender = SmsSenderFactory.createSender(cv("SMS_SERVICE")) sms_sender.send_sms(from_number=from_number, to_number=to_number, msg=m) elif method == "authenticator" or method == "password": # tokens are generated automatically with authenticator apps # and passwords are well passwords # Still go ahead and notify signal receivers that they requested it. code = None us_security_token_sent.send( current_app._get_current_object(), _async_wrapper=current_app.ensure_sync, user=user, method=method, token=code, login_token=code, phone_number=phone_number, send_magic_link=send_magic_link, ) flask-security-5.7.1/flask_security/username_util.py000066400000000000000000000063631511046741400227610ustar00rootroot00000000000000""" flask_security.username_util ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Utility class providing methods for validating and normalizing usernames. :copyright: (c) 2020-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from __future__ import annotations import typing as t import unicodedata from .utils import ( config_value as cv, get_message, ) if t.TYPE_CHECKING: # pragma: no cover import flask class UsernameUtil: """ Utility class providing methods for validating and normalizing usernames. To provide your own implementation, pass in the class as ``username_util_cls`` at init time. Your class will be instantiated once as part of app initialization. .. versionadded:: 4.1.0 """ def __init__(self, app: flask.Flask): """Instantiate class. :param app: The Flask application being initialized. """ pass def check_username(self, username: str) -> str | None: """ Given a username - check for allowable character categories. This is broken out so applications can easily override this method only. By default, allow letters and numbers (using unicodedata.category). Returns None if allowed, error message if not allowed. """ cats = [unicodedata.category(c)[0] for c in username] if any([cat not in ["L", "N"] for cat in cats]): return get_message("USERNAME_DISALLOWED_CHARACTERS")[0] return None def normalize(self, username: str) -> str: """ Given an input username - return a clean (using bleach) and normalized (using Python's unicodedata.normalize()) version. Must be called in app context and uses :py:data:`SECURITY_USERNAME_NORMALIZE_FORM` config variable. """ import bleach if not username: return "" username = bleach.clean(username.strip(), strip=True) if not username: return "" cf = cv("USERNAME_NORMALIZE_FORM") if cf: return unicodedata.normalize(cf, username) return username def validate(self, username: str) -> tuple[str | None, str | None]: """ Username validation. Called in app/request context. The username is first validated then normalized. Input is restricted/validated via a call to check_username. Return value is a tuple (msg, normalized_username). msg will be None if properly validated. It is important that None be returned if data is an empty string since otherwise DBs will complain since the field is unique/nullable. """ import bleach if not username: return None, None uclean = bleach.clean(username.strip(), strip=True) if uclean != username: return get_message("USERNAME_ILLEGAL_CHARACTERS")[0], None msg = self.check_username(uclean) if msg: return msg, None unorm = self.normalize(username) umin = cv("USERNAME_MIN_LENGTH") umax = cv("USERNAME_MAX_LENGTH") if len(unorm) < umin or len(unorm) > umax: return get_message("USERNAME_INVALID_LENGTH", min=umin, max=umax)[0], unorm return None, unorm flask-security-5.7.1/flask_security/utils.py000066400000000000000000001441231511046741400212420ustar00rootroot00000000000000""" flask_security.utils ~~~~~~~~~~~~~~~~~~~~ Flask-Security utils module :copyright: (c) 2012-2019 by Matt Wright. :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from __future__ import annotations import abc import base64 from datetime import datetime, timedelta, timezone from functools import partial import hashlib import hmac import time import typing as t from urllib.parse import parse_qsl, quote, urlsplit, urlunsplit, urlencode import urllib.request import urllib.error import warnings from flask import ( Response, after_this_request, current_app, flash, g, redirect, request, render_template, session, url_for, ) 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_principal import AnonymousIdentity, Identity, identity_changed, Need from flask_wtf import csrf, FlaskForm from wtforms import ValidationError from itsdangerous import BadSignature, SignatureExpired from werkzeug.local import LocalProxy from werkzeug.datastructures import MultiDict from .quart_compat import best, get_quart_status from .proxies import _security, _datastore, _pwd_context, _hashing_context from .signals import user_authenticated if t.TYPE_CHECKING: # pragma: no cover from flask import Flask from flask.typing import ResponseValue from flask_security import UserMixin localize_callback = LocalProxy(lambda: _security.i18n_domain.gettext) FsPermNeed = partial(Need, "fsperm") FsPermNeed.__doc__ = """A need with the method preset to `"fsperm"`.""" def _(translate): """Identity function to mark strings for translation.""" return translate def get_request_attr(name: str) -> t.Any: """Retrieve a request local attribute. Current 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 .. versionchanged:: 4.1.5 Use 'g' rather than request_ctx stack which is going away post Flask 2.2 """ return getattr(g, name, None) def set_request_attr(name: str, value: t.Any) -> None: """Set an attribute on Flask's application context global object (g). :param name: The key/attribute name to store the value under :param value: The value to store in the application context """ return setattr(g, 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 aware_utcnow() -> datetime: """Return a timezone-aware UTC datetime object. From a miguel grinberg blog around dealing with 3.12. Our default SQLAlchemy Datetime is naive. Note that most code should call _security.datetime_factory() :return: Current UTC datetime with timezone information :rtype: datetime """ return datetime.now(timezone.utc) def aware_utcfromtimestamp(timestamp: float) -> datetime: """Create timezone-aware UTC datetime from timestamp. :param timestamp: Unix timestamp (seconds since epoch) :type timestamp: float :return: UTC datetime with timezone information :rtype: datetime """ return datetime.fromtimestamp(timestamp, timezone.utc) def naive_utcnow() -> datetime: """Return a naive UTC datetime (tzinfo removed). :return: Current UTC datetime without timezone information :rtype: datetime """ return aware_utcnow().replace(tzinfo=None) def naive_utcfromtimestamp(timestamp: float) -> datetime: """Create naive UTC datetime from timestamp. :param timestamp: Unix timestamp (seconds since epoch) :type timestamp: float :return: UTC datetime without timezone information :rtype: datetime """ return aware_utcfromtimestamp(timestamp).replace(tzinfo=None) def find_csrf_field_name() -> t.Optional[str]: """Retrieve the configured CSRF field name from Flask-WTF form configuration. This is needed to properly clear CSRF tokens on logout since Flask-WTF doesn't automatically handle this case. The field name can be configured through Flask-WTF's settings or overridden in form classes. :return: Configured CSRF field name if found, None otherwise :rtype: Optional[str] Note: Uses the field name from the login form as set by the Flask-WTF configuration. Requires a DummyForm class with Flask-WTF's meta configuration. """ from .forms import DummyForm form = DummyForm(formdata=None) if hasattr(form.meta, "csrf_field_name"): return form.meta.csrf_field_name return None def is_user_authenticated(user: UserMixin | None) -> bool: """ return True if user is authenticated. With Flask-Login <=0.6.x and Flask-Security <5.4 current_user was always set - for non-authenticated users it pointed to an AnonymousUser Flask-Login is experimenting (11/5/23) with a LOGIN_NO_ANONYMOUS which will set current_user to None and deprecate is_authenticated (current_user non None implies authenticated). We have a configuration variable ANONYMOUS_USER_DISABLED which if true will force current_user to None on unauthenticated as well """ if config_value("ANONYMOUS_USER_DISABLED"): # Note that user often is current_user which is a proxy and isn't ever actually # 'None' return bool(user) return bool(user and user.is_authenticated) def login_user( user: UserMixin, remember: bool | None = None, authn_via: list[str] | None = None, ) -> bool: """Perform the login routine. If :py:data:`SECURITY_TRACKABLE` is used, make sure you commit changes after this request (i.e. ``app.security.datastore.commit()``). :param user: The user to login :param remember: Flag specifying if the remember cookie should be set. If ``None`` use value of :py:data:`SECURITY_DEFAULT_REMEMBER_ME` :param authn_via: A list of strings denoting which mechanism(s) the user authenticated with. These should be one or more of ["password", "sms", "authenticator", "email"] or other 'auto-login' mechanisms. :return: True if user successfully logged in. """ if remember is None: remember = config_value("DEFAULT_REMEMBER_ME") if not _login_user(user, remember): # pragma: no cover return False if _security.trackable: remote_addr = request.remote_addr or None # make sure it is None old_current_login, new_current_login = ( user.current_login_at, _security.datetime_factory(), ) old_current_ip, new_current_ip = user.current_login_ip, remote_addr user.last_login_at = old_current_login or new_current_login user.current_login_at = new_current_login user.last_login_ip = old_current_ip user.current_login_ip = new_current_ip user.login_count = user.login_count + 1 if user.login_count else 1 _datastore.put(user) session["fs_cc"] = "set" # CSRF cookie session["fs_paa"] = time.time() # Primary authentication at - timestamp identity_changed.send( current_app._get_current_object(), # type: ignore[attr-defined] _async_wrapper=current_app.ensure_sync, # type: ignore[arg-type] identity=Identity(user.fs_uniquifier), ) user_authenticated.send( current_app._get_current_object(), # type: ignore[attr-defined] _async_wrapper=current_app.ensure_sync, # type: ignore[arg-type] user=user, authn_via=authn_via, ) return True def logout_user() -> None: """Logs out the current user. This will also clean up the remember me cookie if it exists. This sends an ``identity_changed`` signal to note that the current identity is now the `AnonymousIdentity` """ for key in ( "identity.name", "identity.auth_type", "fs_paa", "fs_gexp", "fs_oauth_next", ): session.pop(key, None) # Clear csrf token between sessions. # Ideally this would be handled by Flask-WTF but... # We don't clear entire session since Flask-Login seems to like having it. csrf_field_name = find_csrf_field_name() if csrf_field_name: session.pop(csrf_field_name, None) # Flask-WTF 'caches' csrf_token - and only set the session if not already # in 'g'. Be sure to clear both. This affects at least /confirm g.pop(csrf_field_name, None) session["fs_cc"] = "clear" identity_changed.send( current_app._get_current_object(), # type: ignore _async_wrapper=current_app.ensure_sync, identity=AnonymousIdentity(), ) _logout_user() def check_and_update_authn_fresh( within: timedelta, grace: timedelta, method: str | None = None, ) -> bool: """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'. :param grace: A timedelta that, if the current session is considered 'fresh' 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. This is not supported for authentication tokens. :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. within.total_seconds() == 0 results in undefined behavior. If "fs_gexp" is in the session and the current timestamp is less than that, return True and extend grace time (i.e. set fs_gexp to current time + grace). Be aware that for this to work, state is required to be sent from the client. Flask security adds this state to the session (cookie) and the auth token. Without this state, 'False' is always returned - (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. .. versionchanged:: 5.5.0 Grab 'Primary Authenticated At' from request_attrs which is set from either session or auth token """ if method == "basic": return True if within.total_seconds() < 0: # this means 'always fresh' return True if not (paa := get_request_attr("fs_paa")): # No recorded primary authenticated at time, you can't play. return False now = naive_utcnow() new_exp = now + grace grace_ts = int(new_exp.timestamp()) if fs_gexp := session.get("fs_gexp", None): if now.timestamp() < fs_gexp: # Within grace period - extend it, and we're good. session["fs_gexp"] = grace_ts return True authn_time = naive_utcfromtimestamp(paa) # allow for some time drift where it's possible authn_time is in the future # but let's be cautious and not allow arbitrary future times delta = now - authn_time if within > delta > -within: session["fs_gexp"] = grace_ts return True return False def get_hmac(password: str | bytes) -> bytes: """Returns a Base64 encoded HMAC+SHA512 of the password signed with the salt specified by :py:data:`SECURITY_PASSWORD_SALT`. :param password: The password to sign """ if not (salt := config_value("PASSWORD_SALT")): raise RuntimeError( "The configuration value `SECURITY_PASSWORD_SALT` must " "not be None when the value of `SECURITY_PASSWORD_HASH` is " 'set to "%s"' % config_value("PASSWORD_HASH") ) h = hmac.new(encode_string(salt), encode_string(password), hashlib.sha512) return base64.b64encode(h.digest()) def verify_password(password: str | bytes, password_hash: str | bytes) -> bool: """Returns ``True`` if the password matches the supplied hash. :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) return _pwd_context.verify(password, password_hash) def verify_and_update_password(password: str | bytes, user: UserMixin) -> bool: """Returns ``True`` if the password is valid for the specified user. Additionally, the hashed password in the database is updated if the hashing algorithm happens to have changed. N.B. you MUST call DB commit if you are using a session-based datastore (such as SqlAlchemy) since the user instance might have been altered (i.e. ``app.security.datastore.commit()``). This is usually handled in the view. :param password: A plaintext password to verify :param user: The user to verify against .. tip:: This should not be called directly - rather use :meth:`.UserMixin.verify_and_update_password` """ # Capture the original input in case we need to pass the unaltered # value to hash_password if the hashing algorithm has changed input_password = password if use_double_hash(user.password): password = get_hmac(password) if _pwd_context.identify(user.password) == "bcrypt": password = password[:72] verified = _pwd_context.verify(password, user.password) else: # Try with original password. verified = _pwd_context.verify(password, user.password) if verified and (user.password is None or _pwd_context.needs_update(user.password)): user.password = hash_password(input_password) _datastore.put(user) return verified def hash_password(password: str | bytes) -> str: """Hash the specified plaintext password. Unless the hash algorithm (as specified by :py:data:`SECURITY_PASSWORD_HASH`) is listed in the configuration variable :py:data:`SECURITY_PASSWORD_SINGLE_HASH`, perform a double hash - first create an HMAC from the plaintext password and the value of :py:data:`SECURITY_PASSWORD_SALT`, then use the configured hashing algorithm. This satisfies OWASP/ASVS section 2.4.5: 'provide additional iteration of a key derivation'. .. versionadded:: 2.0.2 .. versionchanged:: 5.7.0 Explicit check for bcrypt truncation :param password: The plaintext password to hash """ if use_double_hash(): password = get_hmac(password).decode("ascii") # Passing in options as part of hash is deprecated in passlib 1.7 # and new algorithms like argon2 don't even support it. if config_value("PASSWORD_HASH") == "bcrypt": # bcrypt - OWASP says truncation concerns are negligible: # https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#input-limits-of-bcrypt password = password[:72] return _pwd_context.hash( password, **config_value("PASSWORD_HASH_OPTIONS", default={}).get( config_value("PASSWORD_HASH"), {} ), ) def encode_string(string: t.Union[str, bytes]) -> bytes: """Encodes a string to bytes, if it isn't already. :param string: The string to encode :return: UTF-8 encoded bytes """ if isinstance(string, str): string = string.encode("utf-8") return string def hash_data(data: t.Union[str, bytes]) -> str: """Hashes input data after ensuring proper encoding. :param data: Input data to hash (will be encoded if not already bytes) :return: Hashed data as bytes Note: Uses application's configured _hashing_context """ return _hashing_context.hash(encode_string(data)) def verify_hash(hashed_data: bytes, compare_data: t.Union[str, bytes]) -> bool: """Verifies data against a previously hashed value. :param hashed_data: Previously hashed data to compare against :param compare_data: Input data to verify (will be encoded if not already bytes) :return: True if data matches hash, False otherwise Note: Uses application's configured _hashing_context """ return _hashing_context.verify(encode_string(compare_data), hashed_data) def suppress_form_csrf(): """ Return meta contents if we should suppress form from attempting to validate CSRF. If app doesn't want CSRF for unauth endpoints then check if caller is authenticated or not (many endpoints can be called either way). """ if config_value("CSRF_IGNORE_UNAUTH_ENDPOINTS") and not is_user_authenticated( current_user ): return {"csrf": False} return {} def do_flash(message: str, category: str) -> None: """Flash a message depending on if the `FLASH_MESSAGES` configuration value is set. :param message: The flash message :param category: The flash message category """ if config_value("FLASH_MESSAGES"): flash(message, category) def parse_auth_token(auth_token: str) -> dict[str, t.Any]: """Parse an authentication token. This will raise an exception if not properly signed or expired """ tdata = dict() # This can raise BadSignature or SignatureExpired exceptions from itsdangerous raw_data = _security.remember_token_serializer.loads( auth_token, max_age=config_value("TOKEN_MAX_AGE") ) # Version 3.x generated tokens that map to data with 3 elements, # and fs_uniquifier was on last element. # Version 4.0.0 generates tokens that map to data with only 1 element, # which maps to fs_uniquifier. # Version 5 and up are already a dict (with a version #) if isinstance(raw_data, dict): # new format - starting at ver=5 if not all(k in raw_data for k in ["ver", "uid", "exp"]): raise ValueError("Token missing keys") tdata = raw_data if ts := tdata.get("exp"): if ts < int(time.time()): raise SignatureExpired("token[exp] value expired") else: # old tokens that were lists if len(raw_data) == 1: # version 4 tdata["ver"] = "4" tdata["uid"] = raw_data[0] else: # version 3 tdata["ver"] = "3" tdata["uid"] = raw_data[2] return tdata def get_url(endpoint_or_url: str, qparams: dict[str, str] | None = None) -> str: """Returns a URL if a valid endpoint is found. Otherwise, returns the provided value. .. warning:: If an endpoint ISN'T provided, then it is assumed that the URL is external to Flask and if the spa configuration REDIRECT_HOST is set will redirect to that host. This could be an issue in development. :param endpoint_or_url: The endpoint name or URL to default to :param qparams: additional query params to add to end of url :return: URL """ try: return transform_url(url_for(endpoint_or_url), qparams) except Exception: # This is an external URL (no endpoint defined in app) # For (mostly) testing - allow changing/adding the url - for example # add a different host:port for cases where the UI is running # separately. if config_value("REDIRECT_HOST"): url = transform_url( endpoint_or_url, qparams, netloc=config_value("REDIRECT_HOST") ) else: url = transform_url(endpoint_or_url, qparams) return url def slash_url_suffix(url: str, suffix: str) -> str: """ Formats a suffix to be appended to a URL, ensuring proper slash placement. If the given `url` ends with a slash, this function adds a trailing slash to the `suffix`. Otherwise, it adds a leading slash to the `suffix`. This helps prevent double slashes or missing slashes when constructing URLs. :param url: The base URL to which the suffix will be appended. :param suffix: The suffix to be appended to the URL. :return: The formatted suffix with the appropriate leading or trailing slash. Example: >>> slash_url_suffix("https://example.com/api", "v1") '/v1' >>> slash_url_suffix("https://example.com/api/", "v1") 'v1/' """ return url.endswith("/") and f"{suffix}/" or f"/{suffix}" def transform_url( url: str, qparams: dict[str, str] | None = None, **kwargs: str ) -> str: """Modify url :param url: url to transform (can be relative) :param qparams: additional query params to add to end of url :param kwargs: pieces of URL to modify - e.g. netloc=localhost:8000 :return: Modified URL .. versionadded:: 3.2.0 """ link_parse = urlsplit(url) if qparams: current_query = dict(parse_qsl(link_parse.query)) current_query.update(qparams) link_parse = link_parse._replace(query=urlencode(current_query)) return urlunsplit(link_parse._replace(**kwargs)) def get_security_endpoint_name(endpoint: str) -> str: """ Returns the fully qualified endpoint name by combining the blueprint name and the endpoint. :param endpoint: The endpoint name to be combined with the blueprint name. :return: The fully qualified endpoint name in the format '.'. Example: >>> get_security_endpoint_name("login") 'my_blueprint.login' """ return f"{config_value('BLUEPRINT_NAME')}.{endpoint}" def url_for_security(endpoint: str, **values: t.Any) -> str: """Return a URL for the security blueprint :param endpoint: the endpoint of the URL (name of the function) :param values: the variable arguments of the URL rule :param _external: if set to `True`, an absolute URL is generated. Server address can be changed via `SERVER_NAME` configuration variable which defaults to `localhost`. :param _anchor: if provided, this is added as anchor to the URL. :param _method: if provided, this explicitly specifies an HTTP method. """ endpoint = get_security_endpoint_name(endpoint) # mypy is complaining about this - but I think it's wrong? return url_for(endpoint, **values) # type: ignore def validate_redirect_url(url: str) -> bool: """Validate redirect URL In the default configuration only redirects to the same domain (and scheme) are allowed. The REDIRECT_ALLOW_SUBDOMAINS allows ANY subdomain of SERVER_NAME to be a redirect target. The REDIRECT_BASE_DOMAIN and REDIRECT_ALLOWED_SUBDOMAINS allow specifying 'side' redirects. """ if url is None or url.strip() == "": return False 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: 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 base_domain = config_value("REDIRECT_BASE_DOMAIN") if base_domain: allowable = [ f"{sub}.{base_domain}" for sub in config_value("REDIRECT_ALLOWED_SUBDOMAINS") ] return url_next.netloc in allowable return False return True def get_post_action_redirect( config_key: str, next_loc: FlaskForm | MultiDict | dict | None ) -> str: """ There is a security angle here - the result of this method is sent to Flask::redirect() - and we need to be sure that it can't be interpreted as a user-input external URL - that would mean we would have an 'open-redirect' vulnerability. Allowing an absolute redirect is a security issue - a so-called open-redirect. The complexity here is that urlsplit() does pretty well, but browsers even today May 2021 are very lenient in what they accept as URLs - for example: next=\\\\github.com next=%5C%5C%5Cgithub.com next=/////github.com next=%20\\\\github.com next=%20///github.com next=%20//github.com next=%19////github.com - i.e. browser will strip control chars next=%E2%80%8A///github.com - doesn't redirect! That is a unicode thin space. All will result in a null netloc and scheme from urlsplit - however many browsers will gladly strip off uninteresting characters and convert backslashes to forward slashes - and the cases above will actually cause a redirect to github.com Sigh. Some articles claim that a relative url has to start with a '/' - but that isn't strictly true. From: https://datatracker.ietf.org/doc/html/rfc3986#section-5 a relative path can start with a "//", "/", a non-colon, or be empty. So it seems that all the above URLs are valid. By the time we get the URL, it has been unencoded - so we can't really determine if it is 'valid' since it appears that '/'s can appear in the URL if escaped. The solution is to simply 'quote' the path. """ rurl = propagate_next(find_redirect(config_key), next_loc) (scheme, netloc, path, query, fragment) = urlsplit(rurl) safe_url = urlunsplit((scheme, netloc, quote(path), query, fragment)) return safe_url def get_post_login_redirect() -> str: return get_post_action_redirect("SECURITY_POST_LOGIN_VIEW", request.form) def get_post_register_redirect() -> str: return get_post_action_redirect("SECURITY_POST_REGISTER_VIEW", request.form) def get_post_logout_redirect() -> str: return get_post_action_redirect("SECURITY_POST_LOGOUT_VIEW", request.form) def get_post_verify_redirect() -> str: return get_post_action_redirect("SECURITY_POST_VERIFY_VIEW", request.form) def find_redirect(key: str) -> str: """Returns the URL to redirect to. :param key: The application configuration key to search for """ app_url = None if app_value := current_app.config[key.upper()]: app_url = get_url(app_value) rv = app_url or str(current_app.config.get("APPLICATION_ROOT", "/")) return rv def propagate_next(fallback_url: str, form: FlaskForm | MultiDict | dict | None) -> str: """Compute appropriate redirect URL The application can add a 'next' query parameter or have 'next' as a form field. If either exist, make sure they are valid (not pointing to external location) If neither, return the fallback_url Can be passed either request.form (which is really a MultiDict OR a real form OR a dict with a 'next' key). """ form_next = None if form and isinstance(form, FlaskForm): if hasattr(form, "next") and form.next.data: form_next = form.next.data elif form and form.get("next", None): form_next = str(form.get("next")) arg_next = request.args.get("next") urls = [ get_url(form_next) if form_next else None, get_url(arg_next) if arg_next else None, fallback_url, ] for url in urls: if url and validate_redirect_url(url): return url raise ValueError("No valid redirect URL found - configuration error") def simplify_url(base_url: str, redirect_url: str) -> str: """ Reduces the scheme and host from the redirect_url so it can be passed as a relative URL in a query (e.g. next) param. For this method we aren't worrying about a valid url (e.g. if it points externally) - that will be handled by later requests. :param base_url: The URL to simplify 'against'. :param redirect_url: The URL to reduce. """ b_url = urlsplit(base_url) r_url = urlsplit(redirect_url) if (not r_url.scheme or r_url.scheme == b_url.scheme) and ( not r_url.netloc or r_url.netloc == b_url.netloc ): return urlunsplit(("", "", r_url.path, r_url.query, r_url.fragment)) return redirect_url def get_message(key: str, **kwargs: t.Any) -> tuple[str, str]: rv = config_value("MSG_" + key) return localize_callback(rv[0], **kwargs), rv[1] def config_value(key, app=None, default=None, strict=True): """Get a Flask-Security configuration value. :param key: The configuration key without the prefix `SECURITY_` :param app: An optional specific application to inspect. Defaults to Flask's `current_app` :param default: An optional default value if the value is not set :param strict: if True, will raise ValueError if key doesn't exist """ app = app or current_app key = f"SECURITY_{key.upper()}" # protect against spelling mistakes if strict and key not in app.config: raise ValueError(f"Key {key} doesn't exist") return app.config.get(key, default) def get_max_age(key, app=None): td = get_within_delta(key + "_WITHIN", app) return td.seconds + td.days * 24 * 3600 def get_within_delta(key, app=None): """Get a timedelta object from the application configuration following the internal convention of:: Examples of valid config values:: 5 days 10 minutes :param key: The config value key without the `SECURITY_` prefix :param app: Optional application to inspect. Defaults to Flask's `current_app` """ txt = config_value(key, app=app) values = txt.split() return timedelta(**{values[1]: int(values[0])}) def send_mail(subject, recipient, template, **context): """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 it off to :class:`.MailUtil` to actually send the message. """ context.setdefault("security", _security) context.update(_security._run_ctx_processor("mail")) body = None html = None template_path = f"security/email/{template}" if config_value("EMAIL_PLAINTEXT"): body = _security.render_template(f"{template_path}.txt", **context) if config_value("EMAIL_HTML"): html = _security.render_template(f"{template_path}.html", **context) subject = localize_callback(subject) sender = config_value("EMAIL_SENDER") if isinstance(sender, LocalProxy): sender = sender._get_current_object() _security.mail_util.send_mail( template, subject, recipient, sender, body, html, **context, ) def get_token_status(token, serializer, max_age=None, return_data=False): """Get the status of a token. :param token: The token to check :param serializer: The name of the serializer. Can be one of the following: ``confirm``, ``login``, ``reset`` :param max_age: The name of the max age config option. Can be one of the following: ``CONFIRM_EMAIL``, ``LOGIN``, ``RESET_PASSWORD`` .. deprecated:: 5.0.0 """ warnings.warn( "'get_token_status' is deprecated - use check_and_get_token_status instead", DeprecationWarning, stacklevel=2, ) serializer = getattr(_security, serializer + "_serializer") max_age = get_max_age(max_age) user, data = None, None expired, invalid = False, False try: data = serializer.loads(token, max_age=max_age) except SignatureExpired: d, data = serializer.loads_unsafe(token) expired = True except (BadSignature, TypeError, ValueError): invalid = True if data: user = _datastore.find_user(fs_uniquifier=data[0]) expired = expired and (user is not None) if return_data: return expired, invalid, user, data else: return expired, invalid, user def check_and_get_token_status( token: str, serializer_name: str, within: timedelta ) -> tuple[bool, bool, t.Any]: """Get the status of a token and return data. :param token: The token to check :param serializer_name: The name of the serializer. Can be one of the following: ``confirm``, ``login``, ``reset``, ``us_setup`` ``remember``, ``two_factor_validity``, ``wan`` :param within: max age - passed as a timedelta :return: a tuple of (expired, invalid, data) .. versionadded:: 3.4.0 """ serializer = getattr(_security, serializer_name + "_serializer") max_age = within.total_seconds() data = None expired, invalid = False, False try: data = serializer.loads(token, max_age=max_age) except SignatureExpired: d, data = serializer.loads_unsafe(token) expired = True except (BadSignature, TypeError, ValueError): invalid = True return expired, invalid, data def get_identity_attributes(app: Flask | None = None) -> list[str]: # Return list of keys of identity attributes # Is it possible to not have any? app = app or current_app iattrs = app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"] if iattrs: return [[*f][0] for f in iattrs] return [] def get_identity_attribute(attr: str, app: Flask | None = None) -> dict[str, t.Any]: """Given a 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 lookup_identity(identity): """ Lookup identity in DB. This loops through, in order, :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`, and first calls the mapper function to normalize. Then the db.find_user is called on the specified user model attribute. """ 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: str) -> str | None: """Used to normalize a phone number. This is a simple proxy to :py:meth:`PhoneUtil.get_canonical_form` See :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`. .. versionadded:: 3.4.0 """ ph = _security.phone_util.get_canonical_form(identity) return ph def uia_email_mapper(identity: str) -> str | None: """Used to normalize identity as an email. This is a simple proxy to :py:meth:`MailUtil.normalize` :return: Normalized email or None if not valid email. See :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`. .. versionadded:: 3.4.0 """ try: return _security.mail_util.normalize(identity) except ValueError: return None def uia_username_mapper(identity: str) -> str | None: """Used to normalize a username. This is a simple proxy to :py:meth:`UsernameUtil.normalize` See :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`. .. versionadded:: 4.1.0 """ return _security.username_util.normalize(identity) def use_double_hash(password_hash=None): """Return a bool indicating whether a password should be hashed twice.""" # Default to plaintext for backward compatibility with # :py:data:`SECURITY_PASSWORD_SINGLE_HASH` = False single_hash = config_value("PASSWORD_SINGLE_HASH") or {"plaintext"} if password_hash is None: scheme = config_value("PASSWORD_HASH") else: scheme = _pwd_context.identify(password_hash) return not (single_hash is True or scheme in single_hash) def csrf_cookie_handler(response: Response) -> Response: """Called at end of every request. Uses session to track state (set/clear) Ideally we just need to set this once - however by default Flask-WTF has a time-out on these tokens governed by *WTF_CSRF_TIME_LIMIT*. While we could set that to None - and OWASP implies this is fine - that might not be agreeable to everyone. So as a basic usability hack - we check if it is expired and re-generate so at least the user doesn't have to log out and back in (just refresh). We also support a *CSRF_COOKIE_REFRESH_EACH_REQUEST* analogous to Flask's *SESSION_REFRESH_EACH_REQUEST* It is of course removed on logout/session end. Other info on web suggests replacing on every POST and accepting up to 'age' ago. """ csrf_cookie = config_value("CSRF_COOKIE") csrf_cookie_name = config_value("CSRF_COOKIE_NAME") if not csrf_cookie_name: return response op = session.get("fs_cc", None) if not op: 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" ) # Set cookie if successfully logged in with flask_login's remember cookie if has_remember_cookie and is_user_authenticated(current_user): op = "set" else: return response if op == "clear": # Alas delete_cookie only accepts some of the keywords set_cookie does allowed = ["path", "domain", "secure", "httponly", "samesite"] args = {k: csrf_cookie.get(k) for k in allowed if k in csrf_cookie} response.delete_cookie(csrf_cookie_name, **args) session.pop("fs_cc") return response # Send a cookie if any of: # 1) CSRF_COOKIE_REFRESH_EACH_REQUEST is true # 2) fs_cc == "set" - this is on first login # 3) existing cookie has expired send = False if op == "set": send = True session["fs_cc"] = "sent" elif config_value("CSRF_COOKIE_REFRESH_EACH_REQUEST"): send = True elif current_app.config["WTF_CSRF_TIME_LIMIT"]: current_cookie = request.cookies.get(csrf_cookie_name, None) if current_cookie: # Let's make sure it isn't expired if app doesn't set TIME_LIMIT to None. try: csrf.validate_csrf(current_cookie) except ValidationError: send = True if send: response.set_cookie(csrf_cookie_name, value=csrf.generate_csrf(), **csrf_cookie) return response def base_render_json( form: FlaskForm, include_user: bool = True, include_auth_token: bool = False, additional: dict[str, t.Any] | None = None, error_status_code: int = 400, ) -> ResponseValue: """ 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. """ user = getattr(form, "user", None) if form.errors: code = error_status_code # wtforms 3.0 introduces form-level errors - these show up as part of the # errors dict with a key of 'None' payload = json_error_response(field_errors=form.errors) else: code = 200 payload = dict() if user: # This allows anonymous GETs via JSON if include_user: payload["user"] = user.get_security_payload() if include_auth_token: # view willing to return auth_token - check behavior config if ( config_value("BACKWARDS_COMPAT_AUTH_TOKEN") or "include_auth_token" in request.args ): 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 # has it rendered. payload["csrf_token"] = csrf.generate_csrf() if additional: payload.update(additional) return _security._render_json(payload, code, None, user) def simple_render_json( additional: dict[str, t.Any] | None = None, ) -> ResponseValue: payload = dict(csrf_token=csrf.generate_csrf()) if additional: payload.update(additional) return _security._render_json(payload, 200, None, None) def default_want_json(req): """Return True if response should be in json N.B. do not call this directly - use security._want_json() :param req: Flask/Werkzeug Request """ if req.is_json: return True # TODO should this handle json sub-types? accept_mimetypes = req.accept_mimetypes if not hasattr(req.accept_mimetypes, "best"): # pragma: no cover # Alright. we don't have the best property, lets add it ourselves. # This is for quart compatibility accept_mimetypes.best = best if accept_mimetypes.best == "application/json": return True return False def json_error_response( errors: str | list | None = None, field_errors: dict[str | None, list] | None = None, ) -> dict[str, t.Any]: """Helper to create an error response. The "errors" key holds a simple list of errors - which is made up of any passed errors (either a string or list) as well as the (localized) error msgs from the passed in field_errors. The "field_errors" key which is exactly what is returned from WTForms - namely a dict of field-name: msg. For form-level errors (WTForms 3.0) the 'field-name' is None - which alas means it isn't sortable and Flask's default JSONProvider sorts keys - so we change that to '__all__' which is what django uses apparently and was suggested as part of WTForms 3.0. """ response_json: dict[str, list | dict[str, list]] = dict() plain_errors = [] if errors: if isinstance(errors, str): plain_errors = [errors] elif isinstance(errors, list): plain_errors = errors else: raise TypeError("The errors argument should be either a str or list.") if field_errors: # This is default from WTForms - a dictionary of field name and list of errors # we return that, as well as create a simple list of errors. for e in field_errors.values(): plain_errors.extend(e) if None in field_errors.keys(): # Ugh - wtforms decided to use None as a key - which json # a) can't sort # b) converts to "null" # Issue filed - maybe they will change it field_errors[""] = field_errors[None] del field_errors[None] response_json["field_errors"] = field_errors # type: ignore response_json["errors"] = plain_errors return response_json def default_render_template(*args: t.Any, **kwargs: t.Any) -> str: return render_template(*args, **kwargs) class SmsSenderBaseClass(metaclass=abc.ABCMeta): @abc.abstractmethod def send_sms( self, from_number: str, to_number: str, msg: str ) -> None: # pragma: no cover """Abstract method for sending sms messages .. versionadded:: 3.2.0 """ return class DummySmsSender(SmsSenderBaseClass): def send_sms( self, from_number: str, to_number: str, msg: str ) -> None: # pragma: no cover """Do nothing.""" return None class SmsSenderFactory: senders: dict[str, t.Type[SmsSenderBaseClass]] = {"Dummy": DummySmsSender} @classmethod def createSender(cls, name, *args, **kwargs): """Initialize an SMS sender. :param name: Name as registered in SmsSenderFactory:senders (e.g. 'Twilio') .. versionadded:: 3.2.0 """ return cls.senders[name](*args, **kwargs) try: # pragma: no cover from twilio.rest import Client class TwilioSmsSender(SmsSenderBaseClass): def __init__(self): super().__init__() self.account_sid = config_value("SMS_SERVICE_CONFIG")["ACCOUNT_SID"] self.auth_token = config_value("SMS_SERVICE_CONFIG")["AUTH_TOKEN"] def send_sms(self, from_number: str, to_number: str, msg: str) -> None: """Send message via twilio account.""" client = Client(self.account_sid, self.auth_token) client.messages.create(to=to_number, from_=from_number, body=msg) SmsSenderFactory.senders["Twilio"] = TwilioSmsSender except Exception: pass def password_length_validator(password: str) -> list[str] | None: """Test password for length. :param password: Plain text password to check :return: ``None`` if password conforms to length requirements, a list of error/suggestions if not. .. versionadded:: 3.4.0 """ if len(password) < config_value("PASSWORD_LENGTH_MIN") or len(password) > 128: return [ get_message( "PASSWORD_INVALID_LENGTH", length=config_value("PASSWORD_LENGTH_MIN") )[0] ] return None def password_complexity_validator( password: str, is_register: bool, **kwargs: t.Any ) -> list[str] | None: """Test password for complexity. Currently just supports 'zxcvbn'. :param password: Plain text password to check :param is_register: if True then kwargs are arbitrary additional info. (e.g. info from a registration form). If False, must be a SINGLE key "user" that corresponds to the current_user. All string values will be extracted and sent to the complexity checker. :param kwargs: :return: ``None`` if password is complex enough, a list of error/suggestions if not. Be aware that zxcvbn does not (easily) provide a way to localize messages. .. versionadded:: 3.4.0 """ if config_value("PASSWORD_COMPLEXITY_CHECKER") == "zxcvbn": import zxcvbn user_info: list[t.Any] = [] if not is_register: for v in kwargs["user"].__dict__.values(): if v and isinstance(v, str): user_info.append(v) else: # This is usually all register form values that are in the user_model if kwargs: user_info = list(kwargs.values()) results = zxcvbn.zxcvbn(password, user_inputs=user_info) if results["score"] >= config_value("ZXCVBN_MINIMUM_SCORE"): return None # Should we return suggestions? Default forms don't really know what to do. if results["feedback"]["warning"]: # Note that these come from zxcvbn and # aren't localizable via Flask-Security return [results["feedback"]["warning"]] return [get_message("PASSWORD_TOO_SIMPLE")[0]] else: return None def password_breached_validator(password: str) -> list[str] | None: """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`. Uses :meth:`pwned`. :param password: Plain text password to check :return: ``None`` if password passes breached tests, else a list of error messages. .. versionadded:: 3.4.0 """ if pwn := config_value("PASSWORD_CHECK_BREACHED"): try: cnt = pwned(password) if cnt >= config_value("PASSWORD_BREACHED_COUNT"): return [get_message("PASSWORD_BREACHED")[0]] except Exception: if pwn == "strict": return [get_message("PASSWORD_BREACHED_SITE_ERROR")[0]] return None def pwned(password: str) -> int: """ Check password against pwnedpasswords API using k-Anonymity. https://haveibeenpwned.com/API/v3 :return: Count of password in DB (0 means hasn't been compromised) Can raise HTTPError .. versionadded:: 3.4.0 """ def convert_password_tuple(value): hash_suffix, count = value.split(":") return hash_suffix, int(count) sha1 = hashlib.sha1(password.encode("utf8")).hexdigest() req = urllib.request.Request( url=f"https://api.pwnedpasswords.com/range/{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") entries = dict(map(convert_password_tuple, raw.upper().split("\r\n"))) return entries.get(sha1[5:].upper(), 0) def handle_already_auth(form, payload=None): """ Allow already authenticated users. For GET this is useful for single-page-applications on refresh - session still active but need to access user info and csrf-token. For GET with forms - redirect to POST_LOGIN_VIEW. For POST - redirects to POST_LOGIN_VIEW (forms) or returns 400 (json). This does NOT use get_post_login_redirect() so that it doesn't look at 'next' - which can cause infinite redirect loops (see test_basic::test_authenticated_loop) While it's tempting to try to logout the current user and login the new requested user - that simply doesn't work with CSRF. """ if _security._want_json(request): if request.method == "POST": payload = json_error_response( errors=get_message("ANONYMOUS_USER_REQUIRED")[0] ) return _security._render_json(payload, 400, None, None) else: form.user = current_user return base_render_json(form, additional=payload) else: return redirect(get_url(config_value("POST_LOGIN_VIEW"))) flask-security-5.7.1/flask_security/views.py000066400000000000000000001433061511046741400212410ustar00rootroot00000000000000""" flask_security.views ~~~~~~~~~~~~~~~~~~~~ Flask-Security views module :copyright: (c) 2012 by Matt Wright. :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. CSRF is tricky. By default all our forms have CSRF protection built in via Flask-WTF. This is regardless of authentication method or whether the request is Form or JSON based. Form-based 'just works' since when rendering the form (on GET), the CSRF token is automatically populated. We want to handle: - JSON requests where CSRF token is in a header (e.g. X-CSRF-Token) - Option to skip CSRF when using a token to authenticate (rather than session) (CSRF_PROTECT_MECHANISMS) - Option to skip CSRF for 'login'/unauthenticated requests (CSRF_IGNORE_UNAUTH_ENDPOINTS) This is complicated by the fact that the only way to disable form CSRF is to pass in meta={csrf: false} at form instantiation time. Be aware that for CSRF to work, caller MUST pass in session cookie. So for pure API, and no session cookie - there is no way to support CSRF-Login so app must set CSRF_IGNORE_UNAUTH_ENDPOINTS (or use CSRF/session cookie for logging in then once they have a token, no need for cookie). """ from __future__ import annotations from functools import partial import time import typing as t from flask import ( Blueprint, after_this_request, current_app, jsonify, request, session, ) from flask_login import current_user from .changeable import change_user_password from .change_email import change_email, change_email_confirm from .change_username import change_username from .confirmable import ( confirm_email_token_status, confirm_user, send_confirmation_instructions, ) from .decorators import anonymous_user_required, auth_required, unauth_csrf from .forms import ( _setup_methods_xlate, ChangePasswordForm, DummyForm, ForgotPasswordForm, LoginForm, build_form_from_request, build_form, form_errors_munge, ResetPasswordForm, SendConfirmationForm, TwoFactorVerifyCodeForm, TwoFactorSetupForm, TwoFactorRescueForm, UsernameRecoveryForm, ) from .passwordless import login_token_status, send_login_instructions from .proxies import _security, _datastore from .quart_compat import get_quart_status from .signals import tf_profile_changed from .unified_signin import ( us_signin, us_signin_send_code, us_setup, us_setup_validate, us_verify, us_verify_link, us_verify_send_code, ) from .recoverable import ( reset_password_token_status, send_reset_password_instructions, update_password, send_username_recovery_email, ) from .registerable import register_user, register_existing from .recovery_codes import mf_recovery, mf_recovery_codes from .tf_plugin import ( tf_check_state, tf_illegal_state, tf_set_validity_token_cookie, ) from .twofactor import ( complete_two_factor_process, set_rescue_options, tf_clean_session, tf_disable, ) from .utils import ( base_render_json, check_and_update_authn_fresh, check_and_get_token_status, config_value as cv, do_flash, get_identity_attributes, get_message, get_post_login_redirect, get_post_logout_redirect, get_post_register_redirect, get_post_verify_redirect, get_request_attr, get_within_delta, get_url, handle_already_auth, hash_password, is_user_authenticated, localize_callback, login_user, logout_user, propagate_next, send_mail, slash_url_suffix, url_for_security, view_commit, ) from .webauthn import ( has_webauthn, webauthn_delete, webauthn_register, webauthn_register_response, webauthn_signin, webauthn_signin_response, webauthn_verify, webauthn_verify_response, ) if get_quart_status(): # pragma: no cover from quart import make_response, redirect else: from flask import make_response, redirect if t.TYPE_CHECKING: # pragma: no cover from flask.typing import ResponseValue def default_render_json(payload, code, headers, user): """Default JSON response handler.""" # Force Content-Type header to json. if headers is None: headers = dict() headers["Content-Type"] = "application/json" payload = dict(meta=dict(code=code), response=payload) return make_response(jsonify(payload), code, headers) def _ctx(endpoint): return _security._run_ctx_processor(endpoint) @unauth_csrf() def login() -> ResponseValue: """View function for login view""" form = t.cast(LoginForm, build_form_from_request("login_form")) if is_user_authenticated(current_user): return handle_already_auth( form, payload={"identity_attributes": get_identity_attributes()} ) # Clean out any potential old session info - in case of previous # aborted 2FA attempt. tf_clean_session() if form.validate_on_submit(): assert form.user is not None remember_me = form.remember.data if "remember" in form else None response = _security.two_factor_plugins.tf_enter( form.user, remember_me, "password", next_loc=propagate_next(request.url, form), ) if response: return response # two factor not required - login user after_this_request(view_commit) login_user(form.user, remember=remember_me, authn_via=["password"]) if _security._want_json(request): return base_render_json(form, include_auth_token=True) return redirect(get_post_login_redirect()) if request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): # Validation failed - make sure PII error messages are generic fields_to_squash = dict( email=dict(replace_msg="GENERIC_AUTHN_FAILED"), password=dict(replace_msg="GENERIC_AUTHN_FAILED"), ) if hasattr(form, "username"): fields_to_squash["username"] = dict(replace_msg="GENERIC_AUTHN_FAILED") form_errors_munge(form, fields_to_squash) if request.method == "GET": # set CSRF COOKIE if configured. This is the equivalent of forms and # base_render_json always sending the csrf_token session["fs_cc"] = "set" if _security._want_json(request): payload = { "identity_attributes": get_identity_attributes(), } return base_render_json(form, additional=payload) if ( form.requires_confirmation and cv("REQUIRES_CONFIRMATION_ERROR_VIEW") and not cv("RETURN_GENERIC_RESPONSES") ): # Validation failed BECAUSE user needs to confirm assert form.user_authenticated assert form.email.data # email_required validator do_flash(*get_message("CONFIRMATION_REQUIRED")) return redirect( get_url( cv("REQUIRES_CONFIRMATION_ERROR_VIEW"), qparams={"email": form.email.data}, ) ) return _security.render_template( cv("LOGIN_USER_TEMPLATE"), login_user_form=form, identity_attributes=get_identity_attributes(), **_ctx("login"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def verify(): """View function which handles a reauthentication request.""" form = build_form_from_request("verify_form", user=current_user) if form.validate_on_submit(): # form may have called verify_and_update_password() after_this_request(view_commit) # verified - so set freshness time. session["fs_paa"] = time.time() if _security._want_json(request): return base_render_json(form, include_auth_token=True) do_flash(*get_message("REAUTHENTICATION_SUCCESSFUL")) return redirect(get_post_verify_redirect()) webauthn_available = has_webauthn(current_user, cv("WAN_ALLOW_AS_VERIFY")) if _security._want_json(request): payload = { "has_webauthn_verify_credential": webauthn_available, } return base_render_json(form, additional=payload) return _security.render_template( cv("VERIFY_TEMPLATE"), verify_form=form, has_webauthn_verify_credential=webauthn_available, wan_verify_form=build_form("wan_verify_form"), **_ctx("verify"), ) def logout(): """View function which handles a logout request.""" tf_clean_session() if is_user_authenticated(current_user): logout_user() # No body is required - so if a POST and json - return OK if request.method == "POST" and _security._want_json(request): return _security._render_json({}, 200, None, None) return redirect(get_post_logout_redirect()) @anonymous_user_required @unauth_csrf() def register() -> ResponseValue: """View function which handles a registration request.""" if (_security.confirmable or request.is_json) and _security.forms[ "confirm_register_form" ].cls: form_name = "confirm_register_form" else: form_name = "register_form" form = build_form_from_request(form_name) if form.validate_on_submit(): after_this_request(view_commit) did_login = False user = register_user(form) form.user = user # The 'auto-login' feature probably should be removed - I can't imagine # an application that would want random email accounts. It has been like this # since the beginning. Note that we still enforce 2FA - however for unified # signin - we adhere to historic behavior. if not _security.confirmable or cv("LOGIN_WITHOUT_CONFIRMATION"): response = _security.two_factor_plugins.tf_enter( form.user, False, "register", next_loc=propagate_next(request.url, form) ) if response: return response # two factor not required - login user. login_user(user, authn_via=["register"]) did_login = True if not _security._want_json(request): return redirect(get_post_register_redirect()) # Only include auth token if in fact user is permitted to login return base_render_json(form, include_auth_token=did_login) # Here on GET or failed validate if request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): gr = register_existing(form) if gr: if _security._want_json(request): return base_render_json(form) return redirect(get_post_register_redirect()) if _security._want_json(request): return base_render_json(form) return _security.render_template( cv("REGISTER_USER_TEMPLATE"), register_user_form=form, **_ctx("register"), ) @unauth_csrf() def send_login(): """View function that sends login instructions for passwordless login""" form = build_form_from_request("passwordless_login_form") if form.validate_on_submit(): send_login_instructions(form.user) if not _security._want_json(request): do_flash(*get_message("LOGIN_EMAIL_SENT", email=form.user.email)) if _security._want_json(request): return base_render_json(form) return _security.render_template( cv("SEND_LOGIN_TEMPLATE"), send_login_form=form, **_ctx("send_login") ) @anonymous_user_required def token_login(token): """View function that handles passwordless login via a token Like reset-password and confirm - this is usually a GET via an email so from the request we can't differentiate form-based apps from non. """ expired, invalid, user = login_token_status(token) if not user or invalid: m, c = get_message("INVALID_LOGIN_TOKEN") if cv("REDIRECT_BEHAVIOR") == "spa": return redirect(get_url(cv("LOGIN_ERROR_VIEW"), qparams={c: m})) do_flash(m, c) return redirect(url_for_security("login")) if expired: send_login_instructions(user) m, c = get_message("LOGIN_EXPIRED", email=user.email, within=cv("LOGIN_WITHIN")) if cv("REDIRECT_BEHAVIOR") == "spa": return redirect( get_url( cv("LOGIN_ERROR_VIEW"), qparams=user.get_redirect_qparams({c: m}), ) ) do_flash(m, c) return redirect(url_for_security("login")) login_user(user, authn_via=["token"]) after_this_request(view_commit) if cv("REDIRECT_BEHAVIOR") == "spa": return redirect( get_url(cv("POST_LOGIN_VIEW"), qparams=user.get_redirect_qparams()) ) do_flash(*get_message("PASSWORDLESS_LOGIN_SUCCESSFUL")) return redirect(get_post_login_redirect()) @unauth_csrf() def send_confirmation(): """View function which sends confirmation instructions (/confirm).""" form = t.cast( SendConfirmationForm, build_form_from_request("send_confirmation_form") ) if form.validate_on_submit(): send_confirmation_instructions(form.user) if not _security._want_json(request): do_flash(*get_message("CONFIRMATION_REQUEST", email=form.email.data)) elif request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): # Here on GET or failed validate rinfo = dict(email=dict()) form_errors_munge(form, rinfo) # by suppressing errors JSON should return 200 # Check for other errors - for default form - there aren't additional fields # but applications might add some (e.g. recaptcha) if not form.errors: # Make look exactly like successful (e.g. real user) request if not _security._want_json(request): do_flash(*get_message("CONFIRMATION_REQUEST", email=form.email.data)) if _security._want_json(request): # Never include user info since this is an anonymous endpoint. return base_render_json(form, include_user=False) return _security.render_template( cv("SEND_CONFIRMATION_TEMPLATE"), send_confirmation_form=form, **_ctx("send_confirmation"), ) def confirm_email(token): """ View function which handles an email confirmation request. This is always a GET from an email - so for 'spa' must always redirect. """ expired, invalid, user = confirm_email_token_status(token) if not user or invalid or expired: if expired: m, c = get_message( "CONFIRMATION_EXPIRED", within=cv("CONFIRM_EMAIL_WITHIN"), ) else: m, c = get_message("INVALID_CONFIRMATION_TOKEN") if cv("REDIRECT_BEHAVIOR") == "spa": return redirect(get_url(cv("CONFIRM_ERROR_VIEW"), qparams={c: m})) do_flash(m, c) return redirect( get_url(cv("CONFIRM_ERROR_VIEW")) or url_for_security("send_confirmation") ) already_confirmed = user.confirmed_at is not None if already_confirmed: m, c = get_message("ALREADY_CONFIRMED") if cv("REDIRECT_BEHAVIOR") == "spa": # No reason to expose identity info to anyone who has the link return redirect( get_url( cv("CONFIRM_ERROR_VIEW"), qparams={c: m}, ) ) do_flash(m, c) return redirect( get_url(cv("CONFIRM_ERROR_VIEW")) or url_for_security("send_confirmation") ) confirm_user(user) after_this_request(view_commit) m, c = get_message("EMAIL_CONFIRMED") # ? The only case where user is logged in already would be if # LOGIN_WITHOUT_CONFIRMATION if user != current_user: logout_user() if cv("AUTO_LOGIN_AFTER_CONFIRM"): # N.B. this is a (small) security risk if email went to wrong place. # and you have the LOGIN_WITHOUT_CONFIRMATION flag since in that case # you can be logged in and doing stuff - but another person could # get the email. # Note also this goes against OWASP recommendations. response = _security.two_factor_plugins.tf_enter( user, False, "confirm", next_loc=propagate_next(request.url, None) ) if response: do_flash(m, c) return response login_user(user, authn_via=["confirm"]) if cv("REDIRECT_BEHAVIOR") == "spa": return redirect( get_url( cv("POST_CONFIRM_VIEW"), qparams=user.get_redirect_qparams({c: m}), ) ) do_flash(m, c) return redirect( get_url(cv("POST_CONFIRM_VIEW")) or get_url( cv("POST_LOGIN_VIEW") if cv("AUTO_LOGIN_AFTER_CONFIRM") else ".login" ) ) @unauth_csrf() def forgot_password(): """View function that handles a forgotten password request (/reset). This is allowed for either anonymous or authenticated users. The rationale is that often users stay logged in for a long time and might have forgotten their password and might be prompted for it for sensitive operations (/verify). """ form = t.cast(ForgotPasswordForm, build_form_from_request("forgot_password_form")) if form.validate_on_submit(): send_reset_password_instructions(form.user) if not _security._want_json(request): do_flash(*get_message("PASSWORD_RESET_REQUEST", email=form.email.data)) elif request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): # Here on failed validate (POST) and want generic responses rinfo = dict(email=dict()) form_errors_munge(form, rinfo) # by suppressing errors JSON should return 200 # Check for other errors - for default form - there aren't additional fields # but applications might add some (e.g. recaptcha) if not form.errors: # No OTHER errors on form. # Make look exactly like successful (e.g. real user) request hash_password("not-a-password") # reduce timing between successful and not. if not _security._want_json(request): do_flash(*get_message("PASSWORD_RESET_REQUEST", email=form.email.data)) if _security._want_json(request): # Never include user info since this is an anonymous endpoint. return base_render_json(form, include_user=False) if ( form.requires_confirmation and cv("REQUIRES_CONFIRMATION_ERROR_VIEW") and not cv("RETURN_GENERIC_RESPONSES") ): do_flash(*get_message("CONFIRMATION_REQUIRED")) return redirect( get_url( cv("REQUIRES_CONFIRMATION_ERROR_VIEW"), qparams={"email": form.email.data}, ) ) if is_user_authenticated(current_user): form.email.data = current_user.email return _security.render_template( cv("FORGOT_PASSWORD_TEMPLATE"), forgot_password_form=form, **_ctx("forgot_password"), ) @unauth_csrf() def reset_password(token): """View function that handles a reset password request (/reset/). This endpoint can be called either when authenticated or anonymous This is usually called via GET as part of an email link and redirects to a reset-password form. It is called via POST to actually update the password (and then redirects to a post reset/login view) If in either case the token is either invalid or expired it redirects to the 'forgot-password' form. In the case of non-form based configuration: For GET normal case - redirect to RESET_VIEW?token={token} For GET invalid case - redirect to RESET_ERROR_VIEW?error={error} For POST normal/successful case - return 200 with new authentication token For POST error case return 400 """ expired, invalid, user = reset_password_token_status(token) form = t.cast(ResetPasswordForm, build_form_from_request("reset_password_form")) form.user = user if request.method == "GET": if not user or invalid or expired: if expired: m, c = get_message( "PASSWORD_RESET_EXPIRED", within=cv("RESET_PASSWORD_WITHIN"), ) else: m, c = get_message("INVALID_RESET_PASSWORD_TOKEN") if cv("REDIRECT_BEHAVIOR") == "spa": return redirect(get_url(cv("RESET_ERROR_VIEW"), qparams={c: m})) do_flash(m, c) return redirect(url_for_security("forgot_password")) # All good - for SPA - redirect to the ``reset_view`` # Still - don't include PII such as identity and email if someone # intercepts link they still won't necessarily know the login identity # (even though they can change the password!). if cv("REDIRECT_BEHAVIOR") == "spa": return redirect( get_url( cv("RESET_VIEW"), qparams={"token": token}, ) ) # for forms - render the reset password form return _security.render_template( cv("RESET_PASSWORD_TEMPLATE"), reset_password_form=form, reset_password_token=token, **_ctx("reset_password"), ) # This is the POST case. if not user or invalid or expired: if expired: m, c = get_message( "PASSWORD_RESET_EXPIRED", within=cv("RESET_PASSWORD_WITHIN") ) else: m, c = get_message("INVALID_RESET_PASSWORD_TOKEN") if _security._want_json(request): form.form_errors.append(m) return base_render_json(form, include_user=False) else: do_flash(m, c) return redirect(url_for_security("forgot_password")) if form.validate_on_submit(): after_this_request(view_commit) update_password(user, form.password.data) if cv("AUTO_LOGIN_AFTER_RESET"): # backwards compat - really shouldn't do this according to OWASP response = _security.two_factor_plugins.tf_enter( form.user, False, "reset", next_loc=propagate_next(request.url, None) ) if response: return response # two factor not required - just login login_user(user, authn_via=["reset"]) if _security._want_json(request): dummy_form = DummyForm(formdata=None) dummy_form.user = user return base_render_json(dummy_form, include_auth_token=True) else: do_flash(*get_message("PASSWORD_RESET")) return redirect( get_url(cv("POST_RESET_VIEW")) or get_url(cv("POST_LOGIN_VIEW")) ) else: if _security._want_json(request): return _security._render_json({}, 200, None, None) else: do_flash(*get_message("PASSWORD_RESET_NO_LOGIN")) return redirect(get_url(cv("POST_RESET_VIEW")) or get_url(".login")) # validation failure case - for forms - we try again including the token # for non-forms - we just return errors and assume caller remembers token. if _security._want_json(request): return base_render_json(form) return _security.render_template( cv("RESET_PASSWORD_TEMPLATE"), reset_password_form=form, reset_password_token=token, **_ctx("reset_password"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def change_password(): """View function which handles a change password request.""" form = t.cast(ChangePasswordForm, build_form_from_request("change_password_form")) if not current_user.password: # This is case where user registered w/o a password - since we can't # confirm with existing password - make sure fresh using whatever authentication # method they have set up. if not check_and_update_authn_fresh( cv("FRESHNESS"), cv("FRESHNESS_GRACE_PERIOD"), get_request_attr("fs_authn_via"), ): return _security._reauthn_handler( cv("FRESHNESS"), cv("FRESHNESS_GRACE_PERIOD") ) if form.validate_on_submit(): after_this_request(view_commit) change_user_password(current_user._get_current_object(), form.new_password.data) 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(cv("POST_CHANGE_VIEW")) or get_url(cv("POST_LOGIN_VIEW")) ) active_password = True if current_user.password else False if _security._want_json(request): form.user = current_user payload = dict(active_password=active_password) return base_render_json(form, additional=payload) return _security.render_template( cv("CHANGE_PASSWORD_TEMPLATE"), change_password_form=form, active_password=active_password, **_ctx("change_password"), ) @unauth_csrf() def two_factor_setup(): """View function for two-factor setup. This is used both for GET to fetch forms and POST to actually set configuration (and send token). There are 3 cases for setting up: 1) initial login and application requires 2FA 2) changing existing 2FA information 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 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 state via the session as part of login to show a) who and b) that they successfully authenticated. """ form = t.cast(TwoFactorSetupForm, build_form_from_request("two_factor_setup_form")) changing = is_user_authenticated(current_user) if not changing: # This is the initial login case 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"]: # illegal call on this endpoint tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) user = _datastore.find_user(fs_uniquifier=session["tf_user_id"]) if not user: tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) else: # Caller is changing their TFA profile. This requires a 'fresh' authentication # N.B unauth_csrf has done the CSRF check already. if not check_and_update_authn_fresh( cv("FRESHNESS"), cv("FRESHNESS_GRACE_PERIOD"), get_request_attr("fs_authn_via"), ): return _security._reauthn_handler( cv("FRESHNESS"), cv("FRESHNESS_GRACE_PERIOD") ) user = current_user if form.validate_on_submit(): # Before storing in DB and therefore requiring 2FA we need to # make sure it actually works. # Requiring 2FA is triggered by having BOTH tf_totp_secret and # tf_primary_method in the user record (or having the application # global config TWO_FACTOR_REQUIRED) # Until we correctly validate the 2FA - we don't set primary_method in # user model but use the session to store it. pm = form.setup.data if pm == "disable": tf_disable(user) after_this_request(view_commit) if not _security._want_json(request): do_flash(*get_message("TWO_FACTOR_DISABLED")) return redirect(get_url(cv("TWO_FACTOR_POST_SETUP_VIEW"))) else: return base_render_json(form) # Regenerate the TOTP secret on every call of 2FA setup totp = _security.totp_factory.generate_totp_secret() phone = form.phone.data if pm == "sms" else None session["tf_totp_secret"] = totp session["tf_primary_method"] = pm session["tf_state"] = "validating_profile" # currently - state_token only works for changing TFA - not initial login state_token = None if changing: state = { "totp_secret": totp, "method": pm, "phone": phone, } state_token = _security.tf_setup_serializer.dumps(state) json_response = { "tf_state": "validating_profile", # deprecated in 5.5.0 "tf_primary_method": pm, # old "tf_method": pm, "tf_state_token": state_token, } if phone: # TODO dont save here - wait until complete user.tf_phone_number = phone _datastore.put(user) after_this_request(view_commit) if ( pm == "email" or pm == "sms" ): # TODO not sure this is needed - send checks this msg = user.tf_send_security_token( method=pm, totp_secret=totp, phone_number=phone, ) if msg: # send code didn't work form.setup.errors = list() form.setup.errors.append(msg) if _security._want_json(request): 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(totp, 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"], ) if _security._want_json(request): return base_render_json(form, include_user=False, additional=json_response) code_form = build_form("two_factor_verify_code_form") return _security.render_template( cv("TWO_FACTOR_SETUP_TEMPLATE"), two_factor_setup_form=form, two_factor_verify_code_form=code_form, choices=cv("TWO_FACTOR_ENABLED_METHODS"), chosen_method=pm, # do not translate primary_method=localize_callback( _setup_methods_xlate[getattr(user, "tf_primary_method", None)] ), changing=changing, state_token=state_token, **qrcode_values, **_ctx("tf_setup"), ) # We get here on GET and POST with failed validation. choices = cv("TWO_FACTOR_ENABLED_METHODS")[:] if (not cv("TWO_FACTOR_REQUIRED")) and user.tf_primary_method is not None: choices.insert(0, "disable") if _security._want_json(request): # Provide information application/UI might need to render their own form/input json_response = { "tf_required": cv("TWO_FACTOR_REQUIRED"), "tf_primary_method": getattr(user, "tf_primary_method", None), # old "tf_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 = build_form("two_factor_verify_code_form") return _security.render_template( cv("TWO_FACTOR_SETUP_TEMPLATE"), two_factor_setup_form=form, two_factor_verify_code_form=code_form, choices=choices, chosen_method=None, primary_method=localize_callback( _setup_methods_xlate[getattr(user, "tf_primary_method", None)] ), changing=changing, state_token=None, two_factor_required=cv("TWO_FACTOR_REQUIRED"), **_ctx("tf_setup"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def two_factor_setup_validate(token: str) -> ResponseValue: """ Validate new setup. The token is the state variable which is signed and timed and contains all the state that once confirmed will be stored in the user record. Unlike the code in two_factor_token_validation - this works w/o a session. It also is JUST for setting up/changing two factor for an authenticated user. """ form = t.cast( TwoFactorVerifyCodeForm, build_form_from_request("two_factor_verify_code_form") ) expired, invalid, state = check_and_get_token_status( token, "tf_setup", get_within_delta("TWO_FACTOR_SETUP_WITHIN") ) if invalid: m, c = get_message("API_ERROR") if expired: m, c = get_message( "TWO_FACTOR_SETUP_EXPIRED", within=cv("TWO_FACTOR_SETUP_WITHIN") ) if invalid or expired: tf_clean_session() # until we completely remove session based setup/state if _security._want_json(request): form.form_errors.append(m) return base_render_json(form, include_user=False) do_flash(m, c) return redirect(url_for_security("two_factor_setup")) totp_secret = state["totp_secret"] method = state["method"] phone = state["phone"] form.tf_totp_secret = totp_secret form.primary_method = method form.user = current_user if form.validate_on_submit(): tf_clean_session() # until we completely remove session based setup/state after_this_request(view_commit) _datastore.tf_set(current_user, method, totp_secret, phone) # TODO: should validity cookie be removed? extended? left alone? # Currently - leave it alone - meaning cookie still set. tf_profile_changed.send( current_app._get_current_object(), # type: ignore[attr-defined] _async_wrapper=current_app.ensure_sync, user=current_user, method=method, ) if _security._want_json(request): return base_render_json( form, include_user=False, additional=dict( tf_method=method, tf_primary_method=method, tf_phone=current_user.tf_phone_number, ), ) else: do_flash(*get_message("TWO_FACTOR_CHANGE_METHOD_SUCCESSFUL")) return redirect(get_url(cv("TWO_FACTOR_POST_SETUP_VIEW"))) # Code not correct/outdated. if _security._want_json(request): return base_render_json(form, include_user=False) m, c = get_message("TWO_FACTOR_INVALID_TOKEN") do_flash(m, c) return redirect(url_for_security("two_factor_setup")) @unauth_csrf() def two_factor_token_validation(): """View function for two-factor token validation Two cases: 1) normal login case - everything setup correctly; normal 2FA validation 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 In this case we allow a GET to get the specific enter-code form. """ form = t.cast( TwoFactorVerifyCodeForm, build_form_from_request("two_factor_verify_code_form") ) # state info in session pm = session.get("tf_primary_method", None) totp_secret = session.get("tf_totp_secret", None) tf_state = session.get("tf_state", None) tf_user_id = session.get("tf_user_id", None) changing = is_user_authenticated(current_user) if not changing: # This is the normal login case OR initial setup (two factor required) if ( tf_state not in ["ready", "validating_profile"] or (tf_state == "validating_profile" and not all([pm, totp_secret])) or not tf_user_id ): # illegal call on this endpoint tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) user = _datastore.find_user(fs_uniquifier=tf_user_id) form.user = user if not user: tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) if tf_state == "ready": # normal login case - use saved values pm = user.tf_primary_method totp_secret = user.tf_totp_secret else: # Changing TFA profile - user is already authenticated. if tf_state != "validating_profile" or not all([pm, totp_secret]): tf_clean_session() # logout since this seems like attack-ish/logic error logout_user() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) form.user = current_user form.primary_method = pm form.tf_totp_secret = totp_secret if form.validate_on_submit(): # Success - finish process based on 'changing' and clear all session variables completion_message, token = complete_two_factor_process( form.user, pm, totp_secret, changing ) after_this_request(view_commit) if token: after_this_request(partial(tf_set_validity_token_cookie, token=token)) if not _security._want_json(request): do_flash(*get_message(completion_message)) if changing: return redirect(get_url(cv("TWO_FACTOR_POST_SETUP_VIEW"))) else: return redirect(get_post_login_redirect()) else: return base_render_json(form, include_auth_token=True) # GET or not successful POST # if we were trying to validate a new method if changing: if _security._want_json(request): return base_render_json(form) # allow app to fetch just this form (independent of /tf_setup) return _security.render_template( cv("TWO_FACTOR_VERIFY_CODE_TEMPLATE"), two_factor_verify_code_form=form, chosen_method=localize_callback(_setup_methods_xlate[pm]), **_ctx("tf_token_validation"), ) # if we were trying to validate an existing method else: rescue_form = build_form("two_factor_rescue_form") recovery_options = set_rescue_options(rescue_form, form.user) if _security._want_json(request): return base_render_json( form, additional=dict(recovery_options=recovery_options) ) return _security.render_template( cv("TWO_FACTOR_VERIFY_CODE_TEMPLATE"), two_factor_rescue_form=rescue_form, two_factor_verify_code_form=form, chosen_method=localize_callback(_setup_methods_xlate[pm]), problem=None, **_ctx("tf_token_validation"), ) @anonymous_user_required @unauth_csrf() def two_factor_rescue(): """Function that handles a situation where user can't enter his two-factor validation code User must have already provided valid username/password. User must have already established 2FA """ form = t.cast( TwoFactorRescueForm, build_form_from_request("two_factor_rescue_form") ) form.user = tf_check_state(["ready"]) if not form.user: return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) recovery_options = set_rescue_options(form, form.user) rproblem = "" if form.validate_on_submit(): raction = form.help_setup.data rproblem = raction if raction == "email": msg = form.user.tf_send_security_token( method="email", totp_secret=form.user.tf_totp_secret, phone_number=getattr(form.user, "tf_phone_number", None), ) if msg: rproblem = "" form.help_setup.errors.append(msg) if _security._want_json(request): return base_render_json( form, include_user=False, error_status_code=500 ) # drop through to GET path elif raction == "recovery_code": return redirect(url_for_security("mf_recovery")) # send app provider a mail message regarding trouble elif raction == "help": send_mail( cv("EMAIL_SUBJECT_TWO_FACTOR_RESCUE"), cv("TWO_FACTOR_RESCUE_MAIL"), "two_factor_rescue", user=form.user, ) # drop through to GET path else: return "", 404 if _security._want_json(request): return base_render_json( form, include_user=False, additional=dict(recovery_options=recovery_options) ) code_form = build_form("two_factor_verify_code_form") return _security.render_template( cv("TWO_FACTOR_VERIFY_CODE_TEMPLATE"), two_factor_verify_code_form=code_form, two_factor_rescue_form=form, chosen_method=localize_callback( _setup_methods_xlate[form.user.tf_primary_method] ), rescue_mail=cv("TWO_FACTOR_RESCUE_MAIL"), problem=rproblem, **_ctx("tf_token_validation"), ) @anonymous_user_required @unauth_csrf() def recover_username(): """View function for username recovery""" form = t.cast( UsernameRecoveryForm, build_form_from_request("username_recovery_form") ) if form.validate_on_submit(): send_username_recovery_email(form.user) if _security._want_json(request): return base_render_json(form, include_user=False) do_flash(*get_message("USERNAME_RECOVERY_REQUEST")) return redirect(url_for_security("login")) elif request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): rinfo = dict(email=dict()) form_errors_munge(form, rinfo) if not form.errors: if not _security._want_json(request): do_flash(*get_message("USERNAME_RECOVERY_REQUEST")) if _security._want_json(request): return base_render_json(form, include_user=False) return _security.render_template( cv("USERNAME_RECOVERY_TEMPLATE"), username_recovery_form=form, **_ctx("recover_username"), ) def create_blueprint(app, state, import_name): """Creates the security extension blueprint""" bp = Blueprint( cv("BLUEPRINT_NAME", app=app), import_name, url_prefix=cv("URL_PREFIX", app=app), subdomain=cv("SUBDOMAIN", app=app), template_folder="templates", static_folder=cv("STATIC_FOLDER", app), static_url_path=cv("STATIC_FOLDER_URL", app), ) if cv("LOGOUT_METHODS", app=app) is not None: bp.route( cv("LOGOUT_URL", app=app), methods=cv("LOGOUT_METHODS", app=app), endpoint="logout", )(logout) login_url = cv("LOGIN_URL", app=app) if state.passwordless: bp.route(login_url, methods=["GET", "POST"], endpoint="login")(send_login) bp.route( login_url + slash_url_suffix(login_url, ""), endpoint="token_login", )(token_login) elif cv("US_SIGNIN_REPLACES_LOGIN", app=app): bp.route(login_url, methods=["GET", "POST"], endpoint="login")(us_signin) else: bp.route(login_url, methods=["GET", "POST"], endpoint="login")(login) if cv("FRESHNESS", app=app).total_seconds() >= 0: bp.route(cv("VERIFY_URL", app=app), methods=["GET", "POST"], endpoint="verify")( verify ) if state.unified_signin: us_signin_url = cv("US_SIGNIN_URL", app=app) us_signin_send_code_url = cv("US_SIGNIN_SEND_CODE_URL", app=app) us_setup_url = cv("US_SETUP_URL", app=app) us_verify_url = cv("US_VERIFY_URL", app=app) us_verify_send_code_url = cv("US_VERIFY_SEND_CODE_URL", app=app) us_verify_link_url = cv("US_VERIFY_LINK_URL", app=app) bp.route(us_signin_url, methods=["GET", "POST"], endpoint="us_signin")( us_signin ) bp.route( us_signin_send_code_url, methods=["POST"], endpoint="us_signin_send_code", )(us_signin_send_code) bp.route(us_setup_url, methods=["GET", "POST"], endpoint="us_setup")(us_setup) bp.route( us_setup_url + slash_url_suffix(us_setup_url, ""), methods=["POST"], endpoint="us_setup_validate", )(us_setup_validate) # Freshness verification if cv("FRESHNESS", app=app).total_seconds() >= 0: bp.route(us_verify_url, methods=["GET", "POST"], endpoint="us_verify")( us_verify ) bp.route( us_verify_send_code_url, methods=["POST"], endpoint="us_verify_send_code", )(us_verify_send_code) bp.route(us_verify_link_url, methods=["GET"], endpoint="us_verify_link")( us_verify_link ) if state.two_factor: two_factor_setup_url = cv("TWO_FACTOR_SETUP_URL", app=app) two_factor_token_validation_url = cv("TWO_FACTOR_TOKEN_VALIDATION_URL", app=app) two_factor_rescue_url = cv("TWO_FACTOR_RESCUE_URL", app=app) bp.route( two_factor_setup_url, methods=["GET", "POST"], endpoint="two_factor_setup", )(two_factor_setup) bp.route( two_factor_setup_url + slash_url_suffix(two_factor_setup_url, ""), methods=["POST"], endpoint="two_factor_setup_validate", )(two_factor_setup_validate) bp.route( two_factor_token_validation_url, methods=["GET", "POST"], endpoint="two_factor_token_validation", )(two_factor_token_validation) bp.route( two_factor_rescue_url, methods=["GET", "POST"], endpoint="two_factor_rescue", )(two_factor_rescue) if state.registerable: bp.route( cv("REGISTER_URL", app=app), methods=["GET", "POST"], endpoint="register" )(register) if state.recoverable: reset_url = cv("RESET_URL", app=app) bp.route(reset_url, methods=["GET", "POST"], endpoint="forgot_password")( forgot_password ) bp.route( reset_url + slash_url_suffix(reset_url, ""), methods=["GET", "POST"], endpoint="reset_password", )(reset_password) if state.username_recovery: username_recovery_url = cv("USERNAME_RECOVERY_URL", app=app) bp.route( username_recovery_url, methods=["GET", "POST"], endpoint="recover_username", )(recover_username) if state.changeable: bp.route( cv("CHANGE_URL", app=app), methods=["GET", "POST"], endpoint="change_password", )(change_password) if state.change_email: change_email_url = cv("CHANGE_EMAIL_URL", app=app) bp.route( change_email_url, methods=["GET", "POST"], endpoint="change_email", )(change_email) bp.route( change_email_url + slash_url_suffix(change_email_url, ""), methods=["GET"], endpoint="change_email_confirm", )(change_email_confirm) if state.change_username: bp.route( cv("CHANGE_USERNAME_URL", app=app), methods=["GET", "POST"], endpoint="change_username", )(change_username) if state.confirmable: confirm_url = cv("CONFIRM_URL", app=app) bp.route(confirm_url, methods=["GET", "POST"], endpoint="send_confirmation")( send_confirmation ) bp.route( confirm_url + slash_url_suffix(confirm_url, ""), methods=["GET", "POST"], endpoint="confirm_email", )(confirm_email) if cv("MULTI_FACTOR_RECOVERY_CODES", app) and state.support_mfa: multi_factor_recovery_codes_url = cv("MULTI_FACTOR_RECOVERY_CODES_URL", app=app) multi_factor_recovery_url = cv("MULTI_FACTOR_RECOVERY_URL", app=app) bp.route( multi_factor_recovery_codes_url, methods=["GET", "POST"], endpoint="mf_recovery_codes", )(mf_recovery_codes) bp.route( multi_factor_recovery_url, methods=["GET", "POST"], endpoint="mf_recovery", )(mf_recovery) if state.webauthn: wan_register_url = cv("WAN_REGISTER_URL", app=app) wan_signin_url = cv("WAN_SIGNIN_URL", app=app) wan_delete_url = cv("WAN_DELETE_URL", app=app) wan_verify_url = cv("WAN_VERIFY_URL", app=app) bp.route( wan_register_url, methods=["GET", "POST"], endpoint="wan_register", )(webauthn_register) bp.route( wan_register_url + slash_url_suffix(wan_register_url, ""), methods=["POST"], endpoint="wan_register_response", )(webauthn_register_response) bp.route(wan_signin_url, methods=["GET", "POST"], endpoint="wan_signin")( webauthn_signin ) bp.route( wan_signin_url + slash_url_suffix(wan_signin_url, ""), methods=["POST"], endpoint="wan_signin_response", )(webauthn_signin_response) bp.route(wan_delete_url, methods=["GET", "POST"], endpoint="wan_delete")( webauthn_delete ) if cv("FRESHNESS", app=app).total_seconds() >= 0 and cv( "WAN_ALLOW_AS_VERIFY", app=app ): bp.route(wan_verify_url, methods=["GET", "POST"], endpoint="wan_verify")( webauthn_verify ) bp.route( wan_verify_url + slash_url_suffix(wan_verify_url, ""), methods=["POST"], endpoint="wan_verify_response", )(webauthn_verify_response) return bp flask-security-5.7.1/flask_security/webauthn.py000066400000000000000000001050261511046741400217160ustar00rootroot00000000000000""" flask_security.webauthn ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security WebAuthn module :copyright: (c) 2021-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. This implements support for webauthn/FIDO2 Level 2 using the py_webauthn package. Check out: https://golb.hplar.ch/2019/08/webauthn.html for some ideas on recovery and adding additional authenticators. For testing - you can see your YubiKey (or other) resident keys in chrome! chrome://settings/securityKeys Observation: if key isn't resident than Chrome for example won't let you use it if it isn't part of allowedCredentials - throw error: referencing: https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client TODO: - update/add examples to support webauthn - should we universally add endpoint urls to JSON responses? - Add a way to order registered credentials so we can return an ordered list in allowCredentials. - #sctn-usecase-new-device-registration - allow more than one "first" key and have them not necessarily be cross-platform.. add form option? Research: - should we store things like user verified in 'last use'... - By insisting on 2FA if user has registered a webauthn - things get interesting if they try to log in on a different device.... How would they register a security key for a new device? They would need some OTHER 2FA? Force them to register a NEW webauthn key? """ from __future__ import annotations import json import time import typing as t from functools import partial from flask import abort, after_this_request, request, session from flask import current_app from flask_login import current_user from wtforms import BooleanField, HiddenField, RadioField, StringField, SubmitField from .forms import NextFormMixin try: import webauthn from webauthn.authentication.verify_authentication_response import ( VerifiedAuthentication, ) from webauthn.registration.verify_registration_response import VerifiedRegistration from webauthn.helpers import ( parse_registration_credential_json, parse_authentication_credential_json, ) from webauthn.helpers.exceptions import ( InvalidAuthenticationResponse, InvalidJSONStructure, InvalidRegistrationResponse, ) from webauthn.helpers.structs import ( AuthenticatorTransport, PublicKeyCredentialDescriptor, PublicKeyCredentialType, UserVerificationRequirement, ) from webauthn.helpers import bytes_to_base64url except ImportError: # pragma: no cover pass from .decorators import anonymous_user_required, auth_required, unauth_csrf from .forms import ( Form, RequiredLocalize, build_form_from_request, build_form, get_form_field_label, get_form_field_xlate, _setup_methods_xlate, ) from .proxies import _security, _datastore from .quart_compat import get_quart_status from .signals import wan_registered, wan_deleted from .tf_plugin import TfPluginBase, tf_set_validity_token_cookie from .utils import ( _, base_render_json, check_and_get_token_status, config_value as cv, do_flash, get_message, get_post_login_redirect, get_post_verify_redirect, get_url, get_within_delta, login_user, lookup_identity, propagate_next, simple_render_json, url_for_security, view_commit, localize_callback, ) if t.TYPE_CHECKING: # pragma: no cover import flask from flask.typing import ResponseValue from flask_security import Security, UserMixin, WebAuthnMixin if get_quart_status(): # pragma: no cover from quart import redirect else: from flask import redirect class WebAuthnRegisterForm(Form): name = StringField( get_form_field_xlate(_("Nickname")), validators=[RequiredLocalize(message="WEBAUTHN_NAME_REQUIRED")], ) usage = RadioField( get_form_field_xlate(_("Usage")), choices=[ ("first", get_form_field_xlate(_("Use as a first authentication factor"))), ( "secondary", get_form_field_xlate(_("Use as a secondary authentication factor")), ), ], default="secondary", validate_choice=True, ) submit = SubmitField(label=get_form_field_label("submit"), id="wan_register") def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False assert isinstance(self.name.errors, list) inuse = any([self.name.data == cred.name for cred in current_user.webauthn]) if inuse: msg = get_message("WEBAUTHN_NAME_INUSE", name=self.name.data)[0] self.name.errors.append(msg) return False if not cv("WAN_ALLOW_AS_FIRST_FACTOR"): self.usage.data = "secondary" return True class WebAuthnRegisterResponseForm(Form): credential = HiddenField() submit = SubmitField(label=get_form_field_label("submit")) # from state challenge: str name: str usage: str user_verification: bool # this is returned to caller (not part of the client form) registration_verification: VerifiedRegistration transports: list[AuthenticatorTransport] = [] extensions: str def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False # pragma: no cover assert isinstance(self.credential.errors, list) if not self.credential.data: self.credential.errors.append(get_message("API_ERROR")[0]) return False inuse = any([self.name == cred.name for cred in current_user.webauthn]) if inuse: msg = get_message("WEBAUTHN_NAME_INUSE", name=self.name)[0] self.credential.errors.append(msg) return False try: reg_cred = parse_registration_credential_json(self.credential.data) except ( ValueError, KeyError, InvalidJSONStructure, InvalidRegistrationResponse, ): self.credential.errors.append(get_message("API_ERROR")[0]) return False try: self.registration_verification = webauthn.verify_registration_response( credential=reg_cred, expected_challenge=self.challenge.encode(), expected_origin=_security.webauthn_util.origin(), expected_rp_id=request.host.split(":")[0], require_user_verification=self.user_verification, ) if _datastore.find_webauthn(credential_id=reg_cred.raw_id): msg = get_message("WEBAUTHN_CREDENTIAL_ID_INUSE")[0] self.credential.errors.append(msg) return False except KeyError: self.credential.errors.append(get_message("API_ERROR")[0]) return False except InvalidRegistrationResponse as exc: self.credential.errors.append( get_message("WEBAUTHN_NO_VERIFY", cause=str(exc))[0] ) return False self.transports = ( reg_cred.response.transports if reg_cred.response.transports else [] ) # Alas py_webauthn doesn't support extensions response_full = json.loads(self.credential.data) # TODO - verify this is JSON (created with JSON.stringify) self.extensions = response_full.get("extensions", None) return True class WebAuthnSigninForm(Form, NextFormMixin): identity = StringField(get_form_field_label("identity")) remember = BooleanField(get_form_field_label("remember_me")) submit = SubmitField(label=get_form_field_xlate(_("Start")), id="wan_signin") user: UserMixin | None = None # set by caller - is this a second factor authentication? is_secondary: bool def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.remember.default = cv("DEFAULT_REMEMBER_ME") def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False # pragma: no cover user = None if self.is_secondary: if "tf_user_id" in session: user = _datastore.find_user(fs_uniquifier=session["tf_user_id"]) elif cv("WAN_ALLOW_USER_HINTS"): # If we allow HINTS - provide them - but don't error # out if an unknown or disabled account - that would provide too # much 'discovery' capability of un-authenticated users. if self.identity.data: user = lookup_identity(self.identity.data) if user and user.is_active: self.user = user return True class WebAuthnSigninResponseForm(Form, NextFormMixin): """ This form is used both for signin (primary/first or secondary) and verify. """ remember = HiddenField() submit = SubmitField(label=get_form_field_label("submit")) credential = HiddenField() # set by caller challenge: str user_verification: bool is_secondary: bool is_verify: bool # returned to caller authentication_verification: VerifiedAuthentication user: UserMixin | None = None cred: WebAuthnMixin | None = None # Set to True if this authentication qualifies as 'multi-factor' mf_check: bool = False def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False # pragma: no cover assert isinstance(self.credential.errors, list) if not self.credential.data: self.credential.errors.append(get_message("API_ERROR")[0]) return False try: auth_cred = parse_authentication_credential_json(self.credential.data) except ( ValueError, KeyError, InvalidJSONStructure, InvalidAuthenticationResponse, ): self.credential.errors.append(get_message("API_ERROR")[0]) return False # Look up credential Id (raw_id) and user. 7.2.6/7 self.cred = _datastore.find_webauthn(credential_id=auth_cred.raw_id) if not self.cred: self.credential.errors.append( get_message("WEBAUTHN_UNKNOWN_CREDENTIAL_ID")[0] ) return False # This shouldn't be able to happen if datastore properly cascades # delete self.user = _datastore.find_user_from_webauthn(self.cred) if not self.user: # pragma: no cover self.credential.errors.append( get_message("WEBAUTHN_ORPHAN_CREDENTIAL_ID")[0] ) return False # Verify user Handle. 7.2.6 if auth_cred.response.user_handle: if ( auth_cred.response.user_handle != self.user.fs_webauthn_user_handle.encode() ): self.credential.errors.append( get_message("WEBAUTHN_MISMATCH_USER_HANDLE")[0] ) return False # Make sure the usage of credential matches configured if self.is_verify: usage = cv("WAN_ALLOW_AS_VERIFY") elif self.is_secondary: usage = "secondary" else: usage = "first" if not is_cred_usable(self.cred, usage): self.credential.errors.append( get_message("WEBAUTHN_CREDENTIAL_WRONG_USAGE")[0] ) return False if not self.user.is_active: self.credential.errors.append(get_message("DISABLED_ACCOUNT")[0]) return False verify = partial( webauthn.verify_authentication_response, credential=auth_cred, expected_challenge=self.challenge.encode(), expected_origin=_security.webauthn_util.origin(), expected_rp_id=request.host.split(":")[0], credential_public_key=self.cred.public_key, credential_current_sign_count=self.cred.sign_count, ) # Start by verifying requiring user_verification - if that succeeds then # this authn could be used for both primary and secondary. # If it fails, then try to verify with user_verification == False - unless # as part of signin the app required user_verification (as stored in the state) try: self.authentication_verification = verify(require_user_verification=True) self.mf_check = True except InvalidAuthenticationResponse: try: self.authentication_verification = verify( require_user_verification=self.user_verification ) except InvalidAuthenticationResponse as exc: self.credential.errors.append( get_message("WEBAUTHN_NO_VERIFY", cause=str(exc))[0] ) return False return True class WebAuthnDeleteForm(Form): # Change id of name since this shows up on register form that ALSO has a name # element. name = StringField( get_form_field_xlate(_("Nickname")), validators=[RequiredLocalize(message="WEBAUTHN_NAME_REQUIRED")], id="delete-name", ) submit = SubmitField(label=get_form_field_label("delete")) def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False assert isinstance(self.name.errors, list) if not any([self.name.data == cred.name for cred in current_user.webauthn]): self.name.errors.append( get_message("WEBAUTHN_NAME_NOT_FOUND", name=self.name.data)[0] ) return False return True class WebAuthnVerifyForm(Form): submit = SubmitField(label=get_form_field_label("submit"), id="wan_verify") user: UserMixin def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False # pragma: no cover # We are always authenticated - so return possible credentials. self.user = current_user return True @auth_required( lambda: cv("API_ENABLED_METHODS"), within=lambda: cv("FRESHNESS"), grace=lambda: cv("FRESHNESS_GRACE_PERIOD"), ) def webauthn_register() -> ResponseValue: """Start Registration for an existing authenticated user Note that it requires a POST to start the registration and must send 'name' in. We check here that user hasn't already registered an authenticator with that name. Also - this requires that the user already be logged in - so we can provide info as part of the GET that could otherwise be considered leaking user info. """ payload: dict[str, t.Any] form: WebAuthnRegisterForm = t.cast( WebAuthnRegisterForm, build_form_from_request("wan_register_form") ) if form.validate_on_submit(): challenge = _security.webauthn_util.generate_challenge( cv("WAN_CHALLENGE_BYTES") ) if not current_user.fs_webauthn_user_handle: # set a user handle. This allows an easy migration when adding this # column (and not requiring as part of schema change to update all existing # records. New users will have this set as part of user creation. after_this_request(view_commit) _datastore.set_webauthn_user_handle(current_user) ro = dict( challenge=challenge.encode(), rp_name=cv("WAN_RP_NAME"), rp_id=request.host.split(":")[0], user_id=current_user.fs_webauthn_user_handle.encode(), user_name=current_user.calc_username(), timeout=cv("WAN_REGISTER_TIMEOUT"), exclude_credentials=create_credential_list( current_user, ["first", "secondary"] ), ) ro = _security.webauthn_util.registration_options( current_user, form.usage.data, ro ) credential_options = webauthn.generate_registration_options(**ro) co_json = json.loads(webauthn.options_to_json(credential_options)) co_json["extensions"] = {"credProps": True} # If we ask for UserVerification then we need to check that in the response. uv = False if credential_options.authenticator_selection: uv = ( credential_options.authenticator_selection.user_verification == UserVerificationRequirement.REQUIRED ) state = { "challenge": challenge, "name": form.name.data, "usage": form.usage.data, "user_verification": uv, } state_token = _security.wan_serializer.dumps(state) if _security._want_json(request): payload = { "credential_options": co_json, "wan_state": state_token, } return base_render_json(form, include_user=False, additional=payload) return _security.render_template( cv("WAN_REGISTER_TEMPLATE"), wan_register_form=form, wan_register_response_form=build_form("wan_register_response_form"), wan_state=state_token, credential_options=json.dumps(co_json), **_security._run_ctx_processor("wan_register"), ) current_creds = [] cred: WebAuthnMixin for cred in current_user.webauthn: cl = { "name": cred.name, "credential_id": bytes_to_base64url(cred.credential_id), "transports": cred.transports, "lastuse": cred.lastuse_datetime.isoformat(), "usage": cred.usage, "backup_state": ( cred.backup_state if hasattr(cred, "backup_state") else False ), "device_type": ( cred.device_type if hasattr(cred, "device_type") else "Unknown" ), } # TODO: i18n discoverable = "Unknown" if cred.extensions: extensions = json.loads(cred.extensions) if "credProps" in extensions: discoverable = extensions["credProps"].get("rk", "Unknown") cl["discoverable"] = discoverable current_creds.append(cl) payload = {"registered_credentials": current_creds} if _security._want_json(request): return base_render_json(form, additional=payload) return _security.render_template( cv("WAN_REGISTER_TEMPLATE"), wan_register_form=form, wan_delete_form=build_form("wan_delete_form"), registered_credentials=current_creds, **_security._run_ctx_processor("wan_register"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def webauthn_register_response(token: str) -> ResponseValue: """Response from browser.""" form: WebAuthnRegisterResponseForm = t.cast( WebAuthnRegisterResponseForm, build_form_from_request("wan_register_response_form"), ) expired, invalid, state = check_and_get_token_status( token, "wan", get_within_delta("WAN_REGISTER_WITHIN") ) if invalid: m, c = get_message("API_ERROR") if expired: m, c = get_message("WEBAUTHN_EXPIRED", within=cv("WAN_REGISTER_WITHIN")) if invalid or expired: if _security._want_json(request): form.form_errors.append(m) return base_render_json(form, include_user=False) do_flash(m, c) return redirect(url_for_security("wan_register")) form.challenge = state["challenge"] form.name = state["name"] form.usage = state["usage"] form.user_verification = state["user_verification"] if form.validate_on_submit(): # store away successful registration after_this_request(view_commit) _datastore.create_webauthn( current_user._get_current_object(), # Not needed with Werkzeug >2.0.0 name=state["name"], credential_id=form.registration_verification.credential_id, public_key=form.registration_verification.credential_public_key, sign_count=form.registration_verification.sign_count, backup_state=getattr( form.registration_verification, "credential_backed_up", False ), device_type=getattr( form.registration_verification, "credential_device_type", "single_device", ), transports=list(form.transports), extensions=form.extensions, usage=form.usage, ) wan_registered.send( current_app._get_current_object(), # type: ignore _async_wrapper=current_app.ensure_sync, # type: ignore[arg-type] user=current_user, name=state["name"], ) if _security._want_json(request): return base_render_json(form) msg, c = get_message("WEBAUTHN_REGISTER_SUCCESSFUL", name=state["name"]) do_flash(msg, c) return redirect(get_url(cv("WAN_POST_REGISTER_VIEW"))) if _security._want_json(request): return base_render_json(form) if form.errors: for v in form.errors.values(): do_flash(v[0], "error") return redirect(url_for_security("wan_register")) def _signin_common(user: UserMixin | None, usage: list[str]) -> tuple[t.Any, str]: """ Common code between signin and verify - once form has been verified. """ challenge = _security.webauthn_util.generate_challenge(cv("WAN_CHALLENGE_BYTES")) # Populate allowedCredentials if identity passed and allowed allow_credentials = None if user: allow_credentials = create_credential_list(user, usage) ao = dict( rp_id=request.host.split(":")[0], challenge=challenge.encode(), timeout=cv("WAN_SIGNIN_TIMEOUT"), allow_credentials=allow_credentials, ) ao = _security.webauthn_util.authentication_options(user, usage, ao) options = webauthn.generate_authentication_options(**ao) # If we ask for UserVerification then we need to check that in the response. uv = False if options.user_verification == UserVerificationRequirement.REQUIRED: uv = True state = { "challenge": challenge, "user_verification": uv, } o_json = json.loads(webauthn.options_to_json(options)) state_token = t.cast( # type: ignore[redundant-cast] str, _security.wan_serializer.dumps(state) ) return o_json, state_token @anonymous_user_required @unauth_csrf() def webauthn_signin() -> ResponseValue: # This view can be called either as a 'first' authentication or as part of # 2FA. is_secondary = all(k in session for k in ["tf_user_id", "tf_state"]) and session[ "tf_state" ] in ["ready"] if is_secondary or cv("WAN_ALLOW_AS_FIRST_FACTOR"): pass else: abort(404) form = t.cast(WebAuthnSigninForm, build_form_from_request("wan_signin_form")) form.is_secondary = is_secondary if form.validate_on_submit(): o_json, state_token = _signin_common( form.user, ["secondary"] if is_secondary else ["first"] ) if _security._want_json(request): payload = { "credential_options": o_json, "wan_state": state_token, "remember": form.remember.data, "is_secondary": is_secondary, } return base_render_json(form, include_user=False, additional=payload) # Copy the user's remember field into the next form - since that is # auto-submitted. return _security.render_template( cv("WAN_SIGNIN_TEMPLATE"), wan_signin_form=form, wan_signin_response_form=build_form( "wan_signin_response_form", remember=form.remember.data, next=form.next.data, ), wan_state=state_token, credential_options=json.dumps(o_json), is_secondary=is_secondary, **_security._run_ctx_processor("wan_signin"), ) if _security._want_json(request): return base_render_json(form, additional={"is_secondary": is_secondary}) return _security.render_template( cv("WAN_SIGNIN_TEMPLATE"), wan_signin_form=form, wan_signin_response_form=build_form("wan_signin_response_form"), is_secondary=is_secondary, **_security._run_ctx_processor("wan_signin"), ) @unauth_csrf() def webauthn_signin_response(token: str) -> ResponseValue: is_secondary = all(k in session for k in ["tf_user_id", "tf_state"]) and session[ "tf_state" ] in ["ready"] form = t.cast( WebAuthnSigninResponseForm, build_form_from_request("wan_signin_response_form") ) expired, invalid, state = check_and_get_token_status( token, "wan", get_within_delta("WAN_SIGNIN_WITHIN") ) if invalid: m, c = get_message("API_ERROR") if expired: m, c = get_message("WEBAUTHN_EXPIRED", within=cv("WAN_SIGNIN_WITHIN")) if invalid or expired: if _security._want_json(request): form.form_errors.append(m) return base_render_json(form, include_user=False) do_flash(m, c) return redirect(url_for_security("wan_signin")) form.challenge = state["challenge"] form.user_verification = state["user_verification"] form.is_secondary = is_secondary form.is_verify = False if form.validate_on_submit(): # update last use and sign count after_this_request(view_commit) assert form.cred assert form.user form.cred.lastuse_datetime = _security.datetime_factory() form.cred.sign_count = form.authentication_verification.new_sign_count form.cred.backup_state = getattr( form.authentication_verification, "credential_backed_up", False ) form.cred.device_type = getattr( form.authentication_verification, "credential_device_type", "single_device" ) _datastore.put(form.cred) json_payload = {} if is_secondary: tf_token = _security.two_factor_plugins.tf_complete(form.user, True) if tf_token: after_this_request( partial(tf_set_validity_token_cookie, token=tf_token) ) else: # Need Two-factor?: # - Is it required? # - Did this credential provide 2-factor and # is WAN_ALLOW_AS_MULTI_FACTOR set # - Is another 2FA setup? remember_me = bool(form.remember.data) if form.mf_check and cv("WAN_ALLOW_AS_MULTI_FACTOR"): pass else: response = _security.two_factor_plugins.tf_enter( form.user, remember_me, "webauthn", next_loc=propagate_next(request.url, form), ) if response: return response # login user login_user(form.user, remember=remember_me, authn_via=["webauthn"]) goto_url = get_post_login_redirect() if _security._want_json(request): # Tell caller where we would go if forms based - they can use it or # not. json_payload["post_login_url"] = goto_url return base_render_json( form, include_auth_token=True, additional=json_payload ) return redirect(goto_url) # Here on validate error if _security._want_json(request): return base_render_json(form) # Since the response is auto submitted - we go back to # signin form - for now use flash. if form.errors: for v in form.errors.values(): do_flash(v[0], "error") return redirect(url_for_security("wan_signin")) @auth_required( lambda: cv("API_ENABLED_METHODS"), within=lambda: cv("FRESHNESS"), grace=lambda: cv("FRESHNESS_GRACE_PERIOD"), ) def webauthn_delete() -> ResponseValue: """Deletes an existing registered credential.""" form = t.cast(WebAuthnDeleteForm, build_form_from_request("wan_delete_form")) if form.validate_on_submit(): # validate made sure form.name.data exists. cred = [c for c in current_user.webauthn if c.name == form.name.data][0] after_this_request(view_commit) wan_deleted.send( current_app._get_current_object(), # type: ignore _async_wrapper=current_app.ensure_sync, # type: ignore[arg-type] user=current_user, name=cred.name, ) _datastore.delete_webauthn(cred) if _security._want_json(request): return base_render_json(form) msg, c = get_message("WEBAUTHN_CREDENTIAL_DELETED", name=form.name.data) do_flash(msg, c) if _security._want_json(request): return base_render_json(form) if form.name.errors: do_flash(form.name.errors[0], "error") return redirect(url_for_security("wan_register")) @auth_required(lambda: cv("API_ENABLED_METHODS")) def webauthn_verify() -> ResponseValue: """ Re-authenticate to reset freshness time. This is likely the result of a reauthn_handler redirect, which will have filled in ?next=xxx - which we want to carefully not lose as we go through these steps. """ form = t.cast(WebAuthnVerifyForm, build_form_from_request("wan_verify_form")) if form.validate_on_submit(): o_json, state_token = _signin_common(form.user, cv("WAN_ALLOW_AS_VERIFY")) if _security._want_json(request): payload = {"credential_options": o_json, "wan_state": state_token} return base_render_json(form, include_user=False, additional=payload) return _security.render_template( cv("WAN_VERIFY_TEMPLATE"), wan_verify_form=form, wan_signin_response_form=build_form("wan_signin_response_form"), wan_state=state_token, credential_options=json.dumps(o_json), **_security._run_ctx_processor("wan_verify"), ) if _security._want_json(request): return base_render_json(form) return _security.render_template( cv("WAN_VERIFY_TEMPLATE"), wan_verify_form=form, wan_signin_response_form=build_form("wan_signin_response_form"), skip_login_menu=True, **_security._run_ctx_processor("wan_verify"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def webauthn_verify_response(token: str) -> ResponseValue: form = t.cast( WebAuthnSigninResponseForm, build_form_from_request("wan_signin_response_form") ) expired, invalid, state = check_and_get_token_status( token, "wan", get_within_delta("WAN_SIGNIN_WITHIN") ) if invalid: m, c = get_message("API_ERROR") if expired: m, c = get_message("WEBAUTHN_EXPIRED", within=cv("WAN_SIGNIN_WITHIN")) if invalid or expired: if _security._want_json(request): form.form_errors.append(m) return base_render_json(form, include_user=False) do_flash(m, c) return redirect(url_for_security("wan_verify")) form.challenge = state["challenge"] form.user_verification = state["user_verification"] form.is_secondary = False form.is_verify = True if form.validate_on_submit(): # update last use and sign count after_this_request(view_commit) assert form.cred form.cred.lastuse_datetime = _security.datetime_factory() form.cred.sign_count = form.authentication_verification.new_sign_count _datastore.put(form.cred) # verified - so set freshness time. session["fs_paa"] = time.time() if _security._want_json(request): return base_render_json(form, include_auth_token=True) do_flash(*get_message("REAUTHENTICATION_SUCCESSFUL")) return redirect(get_post_verify_redirect()) # Here on validate error (only POST is allowed on this endpoint) if _security._want_json(request): return base_render_json(form) # Since the response is auto submitted - we go back to # verify form - for now use flash. if form.credential.errors: do_flash(form.credential.errors[0], "error") return redirect(url_for_security("wan_verify")) def is_cred_usable(cred: WebAuthnMixin, usage: str | list[str]) -> bool: # Return True is cred can be used for the requested usage/verify if not isinstance(usage, list): usage = [usage] assert "verify" not in usage return cred.usage in usage def has_webauthn(user: UserMixin, usage: str | list[str]) -> bool: # Return True if ``user`` has one or more keys with requested usage. # Usage: either "first" or "secondary" if not isinstance(usage, list): usage = [usage] wan_keys = getattr(user, "webauthn", []) for cred in wan_keys: if is_cred_usable(cred, usage): return True return False def create_credential_list( user: UserMixin, usage: list[str] ) -> list[PublicKeyCredentialDescriptor]: # Return a list of registered credentials - filtered by whether they apply to our # authentication state (first or secondary) cl = [] for cred in user.webauthn: if not is_cred_usable(cred, usage): continue descriptor = PublicKeyCredentialDescriptor( type=PublicKeyCredentialType.PUBLIC_KEY, id=cred.credential_id ) if cred.transports: tlist = cred.transports transports = [AuthenticatorTransport(transport) for transport in tlist] descriptor.transports = transports # TODO order is important - figure out a way to add 'weight' cl.append(descriptor) return cl class WebAuthnTfPlugin(TfPluginBase): def __init__(self, app: flask.Flask): super().__init__(app) def create_blueprint( self, app: flask.Flask, bp: flask.Blueprint, state: Security ) -> None: """Our endpoints are already registered since webauthn can be both a 'first' or 'secondary' authentication mechanism. """ pass def get_setup_methods(self, user: UserMixin) -> list[tuple[str, str]]: if has_webauthn(user, "secondary"): return [("webauthn", localize_callback(_setup_methods_xlate["webauthn"]))] return [] def tf_login( self, user: UserMixin, json_payload: dict[str, t.Any], next_loc: str | None ) -> ResponseValue: session["tf_state"] = "ready" if not _security._want_json(request): values = dict(next=next_loc) if next_loc else dict() return redirect(url_for_security("wan_signin", **values)) # JSON response json_payload["tf_signin_url"] = url_for_security("wan_signin") json_payload["tf_state"] = "ready" json_payload["tf_method"] = "webauthn" return simple_render_json(additional=json_payload) flask-security-5.7.1/flask_security/webauthn_util.py000066400000000000000000000137431511046741400227570ustar00rootroot00000000000000""" flask_security.webauthn_util ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Utility class providing methods controlling various aspects of webauthn. :copyright: (c) 2020-2024 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from __future__ import annotations import secrets import typing as t from flask import current_app, request try: # noinspection PyUnresolvedReferences from webauthn.helpers.structs import ( AuthenticatorAttachment, AuthenticatorSelectionCriteria, ResidentKeyRequirement, UserVerificationRequirement, ) except ImportError: # pragma: no cover pass if t.TYPE_CHECKING: # pragma: no cover import flask from flask_security import UserMixin class WebauthnUtil: """ Utility class allowing an application to fine-tune various Relying Party attributes. To provide your own implementation, pass in the class as ``webauthn_util_cls`` at init time. Your class will be instantiated once as part of app initialization. .. versionadded:: 5.0.0 """ def __init__(self, app: flask.Flask): """Instantiate class. :param app: The Flask application being initialized. """ pass def generate_challenge(self, nbytes: int | None = None) -> str: # Mostly override this for testing, so we can have a 'constant' challenge. return secrets.token_urlsafe(nbytes) def origin(self) -> str: # Return the RP origin - normally this is just the URL of the application. return request.host_url.rstrip("/") def registration_options( self, user: UserMixin, usage: str, existing_options: dict[str, t.Any] ) -> dict[str, t.Any]: """ :param user: User object - could be used to configure on a per-user basis. :param usage: Either "first" or "secondary" (webauthn is being used as a second factor for authentication) :param existing_options: Currently filled in registration options. Return a dict that will be sent in to py-webauthn generate_registration_options """ existing_options["authenticator_selection"] = self.authenticator_selection( user, usage ) return existing_options def authenticator_selection( self, user: UserMixin, usage: str ) -> AuthenticatorSelectionCriteria: """ :param user: User object - could be used to configure on a per-user basis. :param usage: Either "first" or "secondary" (webauthn is being used as a second factor for authentication Part of the registration ceremony is providing information about what kind of authenticators the app is interested in. See: https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dictionary-authenticatorSelection The main options are: - whether you want a ResidentKey (discoverable) - Attachment - platform or cross-platform - Does the key have to provide user-verification :note:: If the key isn't resident then it isn't discoverable which means that the user won't be able to use that key unless they identify themselves (use the key as a second factor OR type in their identity). If they are forced to type in their identity PRIOR to being authenticated, then there is the possibility that the app will leak username information. """ # noqa: E501 select_criteria = AuthenticatorSelectionCriteria() # TODO: look at #sctn-usecase-new-device-registration to see a reason # to allow multiple keys as "first" - only one would need to be cross-platform if usage == "first": select_criteria.authenticator_attachment = ( AuthenticatorAttachment.CROSS_PLATFORM ) select_criteria.user_verification = UserVerificationRequirement.PREFERRED else: # For second factor minimize user-interaction by not asking for UV select_criteria.user_verification = UserVerificationRequirement.DISCOURAGED if not current_app.config.get("SECURITY_WAN_ALLOW_USER_HINTS"): select_criteria.resident_key = ResidentKeyRequirement.REQUIRED else: select_criteria.resident_key = ResidentKeyRequirement.PREFERRED return select_criteria def authentication_options( self, user: UserMixin | None, usage: list[str], existing_options: dict[str, t.Any], ) -> dict[str, t.Any]: """ :param user: User object - could be used to configure on a per-user basis. However, this can be null. :param usage: Either "first" or "secondary" (webauthn is being used as a second factor for authentication) :param existing_options: Currently filled in authentication options. Return a dict that will be sent in to py-webauthn generate_authentication_options """ existing_options["user_verification"] = self.user_verification(user, usage) return existing_options def user_verification( self, user: UserMixin | None, usage: list[str] ) -> UserVerificationRequirement: """ As part of signin - do we want/need user verification. This is called from /wan-signin and /wan-verify :param user: User object - could be used to configure on a per-user basis. Note that this may not be set on initial wan-signin. :param usage: List of "first", "secondary" (webauthn is being used as a second factor for authentication). Note that in the ``verify``/``reauthentication`` case this list is derived from :py:data:`SECURITY_WAN_ALLOW_AS_VERIFY` """ if "secondary" in usage: return UserVerificationRequirement.DISCOURAGED if current_app.config.get("SECURITY_WAN_ALLOW_AS_MULTI_FACTOR"): return UserVerificationRequirement.PREFERRED return UserVerificationRequirement.PREFERRED flask-security-5.7.1/mypy.ini000066400000000000000000000013541511046741400161760ustar00rootroot00000000000000[mypy] pretty = True show_error_codes = True # this sucks - but most of our packages don't yet have types. disable_error_code = import-untyped no_implicit_optional = True disallow_incomplete_defs = True strict_equality = True warn_redundant_casts = True # Can't figure out why it sometimes says its ignored - but PyCharm wants it warn_unused_ignores = False warn_no_return = True warn_unreachable = True # This false-positives with non-annotated methods (like imported packages) warn_return_any = False warn_unused_configs = True [mypy-flask_security.cli] # Due to click 8.1.4 ignore_errors = True [mypy-quart.*] ignore_missing_imports = True [mypy-flask_mailman.*] ignore_missing_imports = True [mypy-twilio.*] ignore_missing_imports = True flask-security-5.7.1/pyproject-too.toml000066400000000000000000000072601511046741400202140ustar00rootroot00000000000000[project] name = "Flask-Security-Too" description = "Quickly add security features to your Flask application." readme.content-type = "text/x-rst" readme.file = "README.rst" keywords = ["flask security"] license = { file = "LICENSE.txt" } maintainers = [{ name = "Chris Wagner", email = "jwag.wagner+github@gmail.com"}] authors = [{ name = "Matt Wright"}, { name = "Chris Wagner", email = "jwag.wagner+github@gmail.com"}] requires-python = ">=3.10" urls.Documentation = "https://flask-security.readthedocs.io" urls.Homepage = "https://github.com/pallets-eco/flask-security" urls.Source = "https://github.com/pallets-eco/flask-security" urls.Tracker = "https://github.com/pallets-eco/flask-security/issues" urls.Releases = "https://pypi.org/project/Flask-Security/" classifiers=[ "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Development Status :: 5 - Production/Stable", ] dynamic = [ "version", ] dependencies = [ # flask dependencies include werkzeug, jinja2, itsdangerous, click, blinker "Flask>=3.1.0", "Flask-Login>=0.6.3", "Flask-Principal>=0.4.0", "Flask-WTF>=1.1.2", "email-validator>=2.0.0", "markupsafe>=2.1.0", "libpass>=1.9.3", "wtforms>=3.0.0", # for form-level errors ] [project.optional-dependencies] babel = ["babel>=2.12.1", "flask_babel>=4.0.0"] fsqla = ["flask_sqlalchemy>=3.1.0", "sqlalchemy>=2.0.18"] common = ["argon2_cffi>=23.1.0", "bcrypt>=4.2.1", "flask_mail>=0.10.0", "bleach>=6.0.0"] mfa = ["cryptography>=42.0.4", "qrcode>=7.4.2", "phonenumberslite>=8.13.11", "webauthn>=2.5.0"] low = [ # Lowest supported versions "Flask==3.1.0", "Flask-SQLAlchemy==3.1.0", "Flask-SQLAlchemy-Lite==0.1.0", "Flask-Babel==4.0.0", "Flask-Mail==0.10.0", "Flask-Login==0.6.3", "Flask-WTF==1.1.2", "peewee==3.17.9", "argon2_cffi==21.3.0", "authlib==1.2.0", "babel==2.12.1", "bcrypt==4.0.1", "bleach==6.0.0", "freezegun", "jinja2==3.1.2", "itsdangerous==2.2.0", "markupsafe==2.1.2", "mongoengine==0.29.1", "mongomock==4.3.0", "pony==0.7.16;python_version<'3.11'", "phonenumberslite==8.13.11", "qrcode==7.4.2", # authlib requires requests "requests", "sqlalchemy==2.0.18", "sqlalchemy-utils==0.41.1", "webauthn==2.0.0", "werkzeug==3.1.0", "zxcvbn==4.4.28" ] [build-system] requires = ["flit_core >=3.8,<4"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "flask_security" [tool.flit.sdist] include = [ "AUTHORS", "CHANGES.rst", "CONTRIBUTING.rst", "LICENSE.txt", ".djlintrc", ".git-blame-ignore-revs", ".gitignore", ".pre-commit-config.yaml", ".readthedocs.yml", "pyproject.toml", "babel.ini", "codecov.yml", "mypy.ini", "pytest.ini", "tox.ini", "docs/", "examples/", "requirements", "tests/", ] exclude = ["docs/_build/"] [tool.djlint] ignore="H005,H006" # lang, img height/width [tool.pyright] include=["flask_security", "tests/view_scaffold.py"] analyzeUnannotatedFunctions = "none" reportMissingImports = false flask-security-5.7.1/pyproject.toml000066400000000000000000000072601511046741400174150ustar00rootroot00000000000000[project] name = "Flask-Security" description = "Quickly add security features to your Flask application." readme.content-type = "text/x-rst" readme.file = "README.rst" keywords = ["flask security"] license = { file = "LICENSE.txt" } maintainers = [{ name = "Chris Wagner", email = "jwag.wagner+github@gmail.com"}] authors = [{ name = "Matt Wright"}, { name = "Chris Wagner", email = "jwag.wagner+github@gmail.com"}] requires-python = ">=3.10" urls.Documentation = "https://flask-security.readthedocs.io" urls.Homepage = "https://github.com/pallets-eco/flask-security" urls.Source = "https://github.com/pallets-eco/flask-security" urls.Tracker = "https://github.com/pallets-eco/flask-security/issues" urls.Releases = "https://pypi.org/project/Flask-Security/" classifiers=[ "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Development Status :: 5 - Production/Stable", ] dynamic = [ "version", ] dependencies = [ # flask dependencies include werkzeug, jinja2, itsdangerous, click, blinker "Flask>=3.1.0", "Flask-Login>=0.6.3", "Flask-Principal>=0.4.0", "Flask-WTF>=1.1.2", "email-validator>=2.0.0", "markupsafe>=2.1.0", "libpass>=1.9.3", "wtforms>=3.0.0", # for form-level errors ] [project.optional-dependencies] babel = ["babel>=2.12.1", "flask_babel>=4.0.0"] fsqla = ["flask_sqlalchemy>=3.1.0", "sqlalchemy>=2.0.18"] common = ["argon2_cffi>=23.1.0", "bcrypt>=4.2.1", "flask_mail>=0.10.0", "bleach>=6.0.0"] mfa = ["cryptography>=42.0.4", "qrcode>=7.4.2", "phonenumberslite>=8.13.11", "webauthn>=2.5.0"] low = [ # Lowest supported versions "Flask==3.1.0", "Flask-SQLAlchemy==3.1.0", "Flask-SQLAlchemy-Lite==0.1.0", "Flask-Babel==4.0.0", "Flask-Mail==0.10.0", "Flask-Login==0.6.3", "Flask-WTF==1.1.2", "peewee==3.17.9", "argon2_cffi==21.3.0", "authlib==1.2.0", "babel==2.12.1", "bcrypt==4.0.1", "bleach==6.0.0", "freezegun", "jinja2==3.1.2", "itsdangerous==2.2.0", "markupsafe==2.1.2", "mongoengine==0.29.1", "mongomock==4.3.0", "pony==0.7.16;python_version<'3.11'", "phonenumberslite==8.13.11", "qrcode==7.4.2", # authlib requires requests "requests", "sqlalchemy==2.0.18", "sqlalchemy-utils==0.41.1", "webauthn==2.0.0", "werkzeug==3.1.0", "zxcvbn==4.4.28" ] [build-system] requires = ["flit_core >=3.8,<4"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "flask_security" [tool.flit.sdist] include = [ "AUTHORS", "CHANGES.rst", "CONTRIBUTING.rst", "LICENSE.txt", ".djlintrc", ".git-blame-ignore-revs", ".gitignore", ".pre-commit-config.yaml", ".readthedocs.yml", "pyproject-too.toml", "babel.ini", "codecov.yml", "mypy.ini", "pytest.ini", "tox.ini", "docs/", "examples/", "requirements", "tests/", ] exclude = ["docs/_build/"] [tool.djlint] ignore="H005,H006" # lang, img height/width [tool.pyright] include=["flask_security", "tests/view_scaffold.py"] analyzeUnannotatedFunctions = "none" reportMissingImports = false flask-security-5.7.1/pytest.ini000066400000000000000000000017371511046741400165350ustar00rootroot00000000000000[pytest] addopts = -rs --cache-clear --strict-markers markers = settings app_settings babel changeable confirmable registerable two_factor recoverable oauth passwordless trackable unified_signin webauthn flask_async csrf change_email change_username username_recovery filterwarnings = error ignore::DeprecationWarning:mongoengine: ignore::DeprecationWarning:flask_login:0 # next for py 3.12 ignore::DeprecationWarning:dateutil:0 ignore:.*passwordless feature.*:DeprecationWarning:flask_security:0 ignore:.*'crypt' is deprecated.*:DeprecationWarning:passlib:0 ignore::DeprecationWarning:pony:0 ignore:.*'sms' was enabled in SECURITY_US_ENABLED_METHODS;.*:UserWarning:flask_security:0 ignore:.*'get_token_status' is deprecated.*:DeprecationWarning:flask_security:0 ignore:.*The SECURITY_USE_REGISTER_V2 configuration option is deprecated.*:DeprecationWarning:flask_security:0 flask-security-5.7.1/requirements/000077500000000000000000000000001511046741400172175ustar00rootroot00000000000000flask-security-5.7.1/requirements/dev.txt000066400000000000000000000004121511046741400205330ustar00rootroot00000000000000-r docs.txt -r tests.txt mypy flit check-wheel-contents psycopg2-binary pymysql pre-commit tox types-requests # for dev - might not install Flask-Security - list those dependencies here flask flask-wtf flask-login flask-principal markupsafe libpass email_validator flask-security-5.7.1/requirements/docs.txt000066400000000000000000000001421511046741400207050ustar00rootroot00000000000000Pallets-Sphinx-Themes Sphinx sphinx-issues packaging Flask-SQLAlchemy sqlalchemy sqlalchemy-utils flask-security-5.7.1/requirements/tests.txt000066400000000000000000000005131511046741400211210ustar00rootroot00000000000000Flask-Babel Babel Flask-Mail Flask-SQLAlchemy Flask-SQLAlchemy-Lite argon2-cffi authlib bcrypt bleach coverage cryptography djlint freezegun mongoengine mongomock msgcheck peewee pony phonenumberslite pydocstyle pytest-cache pytest-cov pytest qrcode # authlib requires requests requests sqlalchemy sqlalchemy-utils webauthn zxcvbn flask-security-5.7.1/tests/000077500000000000000000000000001511046741400156365ustar00rootroot00000000000000flask-security-5.7.1/tests/__init__.py000066400000000000000000000000001511046741400177350ustar00rootroot00000000000000flask-security-5.7.1/tests/conftest.py000066400000000000000000001117531511046741400200450ustar00rootroot00000000000000""" conftest ~~~~~~~~ Test fixtures and what not :copyright: (c) 2017 by CERN. :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from __future__ import annotations from collections.abc import Mapping import sqlite3 import gc import os import tempfile import time import typing as t from datetime import datetime import sys from urllib.parse import urlsplit from passlib.ifc import PasswordHash from passlib.registry import register_crypt_handler import pytest from flask import Flask, Response, jsonify, render_template from flask import request as flask_request from flask_mail import Mail from flask_wtf import CSRFProtect try: from sqlalchemy.orm import Mapped except ImportError: pass from flask_security import ( FSQLALiteUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore, PonyUserDatastore, RoleMixin, Security, SQLAlchemySessionUserDatastore, SQLAlchemyUserDatastore, UserMixin, WebAuthnMixin, auth_required, auth_token_required, http_auth_required, get_request_attr, roles_accepted, roles_required, permissions_accepted, permissions_required, uia_email_mapper, ) from flask_security.utils import localize_callback from tests.test_utils import convert_bool_option, populate_data NO_BABEL = False try: from flask_babel import Babel except ImportError: NO_BABEL = True # enable testing both register form options v2_param = [ pytest.param(dict(use_register_v2=True), id="use_register_v2-True"), pytest.param(dict(use_register_v2=False), id="use_register_v2-False"), ] class FastHash(PasswordHash): """Our own 'hasher'. For testing we want a fast hash, but a real one such that the provided password and hash aren't the same (which is what happens when using plaintext). """ name = "fasthash" setting_kwds = () context_kwds = () @classmethod def hash(cls, secret, **kwds): return f"$fh$1${secret}" @classmethod def verify(cls, secret, stored_hash, **context_kwds): new_hash = f"$fh$1${secret}" return new_hash == stored_hash @classmethod def identify(cls, stored_hash): return stored_hash.startswith("$fh$1$") @classmethod def using(cls, relaxed=False, **settings): return type("fasthash2", (cls,), {}) # python 3.13 is strict about not closing sqlite3 db connections. def find_sqlite_connections(): connections = [] for obj in gc.get_objects(): if isinstance(obj, sqlite3.Connection): connections.append(obj) return connections @pytest.fixture() def app(request): # assert not find_sqlite_connections() # hopefully find tests that don't clean up app = Flask(__name__) app.response_class = Response app.debug = True app.config["SECRET_KEY"] = "secret" 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_TOTP_SECRETS"] = { "1": "TjQ9Qa31VOrfEzuPy4VHQWPCTmRzCnFzMKLxXYiZu9B" } app.config["SECURITY_TOTP_ISSUER"] = "tests" app.config["SECURITY_SMS_SERVICE"] = "test" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SECURITY_PASSWORD_SALT"] = "salty" app.config["SECURITY_CONFIRM_SALT"] = "confirm-salty" # Make this fasthash for most tests - reduces unit test time by 50% app.config["SECURITY_PASSWORD_SCHEMES"] = ["fasthash", "argon2", "bcrypt"] app.config["SECURITY_PASSWORD_HASH"] = "fasthash" app.config["SECURITY_PASSWORD_SINGLE_HASH"] = True register_crypt_handler(FastHash) # Make this hex_md5 for token tests app.config["SECURITY_HASHING_SCHEMES"] = ["hex_md5"] app.config["SECURITY_DEPRECATED_HASHING_SCHEMES"] = [] app.fs_constructor_args = ( dict() ) # allow marks to set items for Security constructor for opt in [ "changeable", "change_email", "change_username", "confirmable", "passwordless", "recoverable", "registerable", "trackable", "two_factor", "unified_signin", "username_recovery", "webauthn", ]: app.config["SECURITY_" + opt.upper()] = opt in request.keywords marker_getter = request.node.get_closest_marker # Import webauthn, or skip test if webauthn isn't installed webauthn_test = marker_getter("webauthn") if webauthn_test is not None: pytest.importorskip("webauthn") app.fs_constructor_args.update(**webauthn_test.kwargs) oauthlib_test = marker_getter("oauth") if oauthlib_test is not None: pytest.importorskip("authlib") mfa_test = marker_getter("two_factor") or marker_getter("unified_signin") if mfa_test is not None: pytest.importorskip("cryptography") flask_async_test = marker_getter("flask_async") if flask_async_test is not None: pytest.importorskip("asgiref") # from flask[async] # allow parameterized tests to set security config variables if hasattr(request, "param") and isinstance(request.param, Mapping): for key, value in request.param.items(): app.config["SECURITY_" + key.upper()] = value # Override config settings as requested for this test settings = marker_getter("settings") if settings is not None: for key, value in settings.kwargs.items(): app.config["SECURITY_" + key.upper()] = value settings = marker_getter("app_settings") if settings is not None: for key, value in settings.kwargs.items(): app.config[key.upper()] = value # allow pytest command line to override everything if request.config.option.setting: for s in request.config.option.setting: key, value = s.split("=") app.config[key.upper()] = convert_bool_option(value) app.mail = Mail(app) # type: ignore # use babel marker to signify tests that need babel extension. babel = marker_getter("babel") if babel: if NO_BABEL: raise pytest.skip("Requires Babel") Babel(app) csrf = marker_getter("csrf") if csrf is not None: # without any keys/arguments - this is the default config # Note that WTF_CSRF_CHECK_DEFAULT = True means Flask_wtf will # run a CSRF check as part of @before_request - before we see it. app.config["WTF_CSRF_ENABLED"] = True if "ignore_unauth" in csrf.kwargs.keys(): app.config["WTF_CSRF_CHECK_DEFAULT"] = False app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True if "csrfprotect" in csrf.kwargs.keys(): # This is needed when passing CSRF in header or non-form input app.config["WTF_CSRF_CHECK_DEFAULT"] = False CSRFProtect(app) @app.route("/") def index(): return render_template("index.html", content="Home Page") @app.route("/profile") @auth_required() def profile(): if hasattr(app, "security"): if app.security._want_json(flask_request): return jsonify(message="profile") return render_template("index.html", content="Profile Page") @app.route("/post_login") @auth_required() def post_login(): return render_template("index.html", content="Post Login") @app.route("/http", methods=["GET", "POST"]) @http_auth_required def http(): return "HTTP Authentication" @app.route("/http_admin_required") @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("/session") @auth_required("session") def session(): return "Session 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") @auth_required("session", "token", "basic") def multi_auth(): return render_template("index.html", content="Session, Token, Basic auth") @app.route("/post_logout") def post_logout(): return render_template("index.html", content="Post Logout") @app.route("/post_register") def post_register(): return render_template("index.html", content="Post Register") @app.route("/post_confirm") def post_confirm(): return render_template("index.html", content="Post Confirm") @app.route("/post_reset") def post_reset(): return render_template("index.html", content="Post Reset") @app.route("/post_change_username") def post_change_username(): return render_template("index.html", content="Post Change Username") @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") @roles_required("admin", "editor") def admin_and_editor(): return render_template("index.html", content="Admin and Editor Page") @app.route("/admin_or_editor") @roles_accepted("admin", "editor") def admin_or_editor(): return render_template("index.html", content="Admin or Editor Page") @app.route("/simple") @roles_accepted("simple") def simple(): return render_template("index.html", content="SimplePage") @app.route("/admin_perm") @permissions_accepted("full-write", "super") def admin_perm(): return render_template( "index.html", content="Admin Page with full-write or super" ) @app.route("/admin_perm_required") @permissions_required("full-write", "super") def admin_perm_required(): return render_template("index.html", content="Admin Page required") @app.route("/page1") def page_1(): return "Page 1" @app.route("/json", methods=["GET", "POST"]) def echo_json(): return jsonify(flask_request.get_json()) @app.route("/json_auth", methods=["POST"]) @auth_required() def echo_jsonauth(): return jsonify(flask_request.get_json()) @app.route("/unauthz", methods=["GET", "POST"]) def unauthz(): return render_template("index.html", content="Unauthorized") @app.route("/fresh", methods=["GET", "POST"]) @auth_required(within=60) def fresh(): if app.security._want_json(flask_request): return jsonify(title="Fresh Only") else: return render_template("index.html", content="Fresh Only") def revert_forms(): # Some forms/tests have dynamic fields - be sure to revert them. if hasattr(app, "security"): for form_name in [ "login_form", "register_form", "confirm_register_form", "change_username_form", ]: if hasattr(app.security.forms[form_name].cls, "username"): del app.security.forms[form_name].cls.username from flask_security import RegisterFormV2 from flask_security.forms import PasswordConfirmFormMixin, NewPasswordFormMixin for attr in ["username"]: if hasattr(RegisterFormV2, attr): delattr(RegisterFormV2, attr) RegisterFormV2.password_confirm = PasswordConfirmFormMixin.password_confirm RegisterFormV2.password = NewPasswordFormMixin.password request.addfinalizer(revert_forms) yield app # help find tests that don't clean up - note that pony leaves a connection so # we can't use this in 'production'... # assert not find_sqlite_connections() @pytest.fixture() def outbox(app): with app.mail.record_messages() as outbox: yield outbox @pytest.fixture() def mongoengine_datastore(app, tmpdir, realmongodburl): ds, td = mongoengine_setup(app, tmpdir, realmongodburl) yield ds td() def mongoengine_setup(app, tmpdir, realmongodburl): # To run against a realdb: mongod --dbpath import pymongo import mongomock from mongoengine import Document, connect from mongoengine.fields import ( BinaryField, BooleanField, DateTimeField, IntField, ListField, ReferenceField, StringField, ) from mongoengine import PULL, CASCADE, disconnect_all db_name = "flask_security_test" db_host = realmongodburl if realmongodburl else "mongodb://localhost" db_client_class = pymongo.MongoClient if realmongodburl else mongomock.MongoClient db = connect( alias=db_name, db=db_name, host=db_host, port=27017, mongo_client_class=db_client_class, ) class Role(Document, RoleMixin): name = StringField(required=True, unique=True, max_length=80) description = StringField(max_length=255) permissions = ListField(required=False) meta = {"db_alias": db_name} class WebAuthn(Document, WebAuthnMixin): credential_id = BinaryField(primary_key=True, max_bytes=1024, required=True) public_key = BinaryField(required=True) sign_count = IntField(default=0) transports = ListField(required=False) backup_state = BooleanField(required=True) device_type = StringField(max_length=64, required=True) # a JSON string as returned from registration extensions = StringField(max_length=255) lastuse_datetime = DateTimeField(required=True) # name is provided by user - we make sure it is unique per user name = StringField(max_length=64, required=True) usage = StringField(max_length=64, required=True) # we need to be able to look up a user from a credential_id user = ReferenceField("User") # user_id = ObjectIdField(required=True) meta = {"db_alias": db_name} def get_user_mapping(self) -> dict[str, str]: """ Return the mapping from webauthn back to User """ return dict(id=self.user.id) class User(Document, UserMixin): email = StringField(unique=True, max_length=255) fs_uniquifier = StringField(unique=True, max_length=64, required=True) fs_webauthn_user_handle = StringField(unique=True, max_length=64) 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) mf_recovery_codes = ListField(required=False) us_totp_secrets = StringField() us_phone_number = StringField( max_length=255, unique=True, required=False, sparse=True ) 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=[]) webauthn = ListField( ReferenceField(WebAuthn, reverse_delete_rule=PULL), default=[] ) meta = {"db_alias": db_name} def get_security_payload(self): return {"email": str(self.email)} User.register_delete_rule(WebAuthn, "user", CASCADE) def tear_down(): with app.app_context(): User.drop_collection() Role.drop_collection() WebAuthn.drop_collection() db.drop_database(db_name) disconnect_all() return MongoEngineUserDatastore(db, User, Role, WebAuthn), tear_down @pytest.fixture() def sqlalchemy_datastore(app, tmpdir, realdburl): ds, td = sqlalchemy_setup(app, tmpdir, realdburl) yield ds td() def sqlalchemy_setup(app, tmpdir, realdburl): pytest.importorskip("flask_sqlalchemy") from flask_sqlalchemy import SQLAlchemy from sqlalchemy import Column, Integer from flask_security.models import fsqla_v3 as fsqla if realdburl: db_url, db_info = _setup_realdb(realdburl) app.config["SQLALCHEMY_DATABASE_URI"] = db_url else: app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" # In Flask-SQLAlchemy >= 3.0.0 queries are no longer logged automatically, # even in debug or testing mode. app.config["SQLALCHEMY_RECORD_QUERIES"] = True db = SQLAlchemy(app) fsqla.FsModels.set_db_info(db) class Role(db.Model, fsqla.FsRoleMixin): pass class WebAuthn(db.Model, fsqla.FsWebAuthnMixin): pass class User(db.Model, fsqla.FsUserMixin): security_number = Column(Integer, unique=True) def get_security_payload(self): # Make sure we still properly hook up to flask's JSON extension # which handles datetime return {"email": str(self.email), "last_update": self.update_datetime} def augment_auth_token(self, tdata): # for testing - if TESTING_AUGMENT_AUTH_TOKEN is set - call that from flask import current_app if cb := current_app.config.get("TESTING_AUGMENT_AUTH_TOKEN"): cb(tdata) with app.app_context(): db.create_all() def tear_down(): with app.app_context(): if realdburl: db.drop_all() db.engine.dispose() # make sure Flask-SQLAlchemy connections are closed _teardown_realdb(db_info) engine = db.engine engine.dispose() return SQLAlchemyUserDatastore(db, User, Role, WebAuthn), tear_down @pytest.fixture() def fsqlalite_datastore(app, tmpdir, realdburl): ds, td = fsqlalite_setup(app, tmpdir, realdburl) yield ds td() @pytest.fixture() def fsqlalite_min_datastore(app, tmpdir, realdburl): pytest.importorskip("flask_sqlalchemy_lite") from sqlalchemy.orm import declared_attr, mapped_column, relationship from sqlalchemy import String class FsMinUserMixin(UserMixin): # flask_security basic fields id: Mapped[int] = mapped_column(primary_key=True) # type: ignore email: Mapped[str] = mapped_column(String(255), unique=True) # type: ignore password: Mapped[str | None] = mapped_column(String(255)) # type: ignore active: Mapped[bool] = mapped_column() # type: ignore fs_uniquifier: Mapped[str] = mapped_column( # type: ignore String(64), unique=True ) @declared_attr def roles(cls): # The first arg is a class name, the backref is a column name return relationship( "Role", secondary="roles_users", back_populates="users", ) ds, td = fsqlalite_setup( app, tmpdir, realdburl, usermixin=FsMinUserMixin, use_webauthn=False ) yield ds td() def fsqlalite_setup(app, tmpdir, realdburl, usermixin=None, use_webauthn=True): pytest.importorskip("flask_sqlalchemy_lite") from flask_sqlalchemy_lite import SQLAlchemy from sqlalchemy.orm import DeclarativeBase, mapped_column from flask_security.models import sqla as sqla if not usermixin: usermixin = sqla.FsUserMixin if realdburl: db_url, db_info = _setup_realdb(realdburl) else: db_url = "sqlite:///:memory:" app.config |= { "SQLALCHEMY_ENGINES": { "default": {"url": db_url, "pool_pre_ping": True}, }, } db = SQLAlchemy(app) class Model(DeclarativeBase): pass sqla.FsModels.set_db_info(base_model=Model) class Role(Model, sqla.FsRoleMixin): __tablename__ = "role" if use_webauthn: class WebAuthn(Model, sqla.FsWebAuthnMixin): __tablename__ = "webauthn" class User(Model, usermixin): __tablename__ = "user" security_number: Mapped[t.Optional[int]] = mapped_column( # type: ignore unique=True ) def get_security_payload(self) -> dict[str, t.Any]: # Make sure we still properly hook up to flask's JSON extension # which handles datetime return {"email": str(self.email), "last_update": self.update_datetime} with app.app_context(): Model.metadata.create_all(db.engine) def tear_down(): with app.app_context(): Model.metadata.drop_all(db.engine) engine = db.engine engine.dispose() if realdburl: _teardown_realdb(db_info) return ( FSQLALiteUserDatastore(db, User, Role, WebAuthn if use_webauthn else None), tear_down, ) @pytest.fixture() def sqlalchemy_session_datastore(app, tmpdir, realdburl): if sys.version_info < (3, 10): pytest.skip("requires python3.10 or higher") ds, td = sqlalchemy_session_setup(app, tmpdir, realdburl) yield ds td() def sqlalchemy_session_setup(app, tmpdir, realdburl, **engine_kwargs): """ Note that we test having a different user id column name here. """ pytest.importorskip("sqlalchemy") from sqlalchemy import create_engine from sqlalchemy.orm import ( mapped_column, scoped_session, sessionmaker, declarative_base, ) from sqlalchemy.ext.declarative import declared_attr from sqlalchemy import ( Column, Integer, ForeignKey, ) from flask_security.models import sqla as sqla if realdburl: db_url, db_info = _setup_realdb(realdburl) engine = db_info["engine"] else: db_url = "sqlite:///:memory:" engine = create_engine(db_url, **engine_kwargs) db_session = scoped_session( sessionmaker(autocommit=False, autoflush=False, bind=engine) ) app.teardown_appcontext(lambda exc: db_session.close()) Base = declarative_base() # Note that in this case we don't call set_db_info since we are using # normal table names AND we need our own RolesUsers table since we modified the # PK names. class WebAuthn(Base, sqla.FsWebAuthnMixin): __tablename__ = "webauthn" @declared_attr def user_id(self) -> Mapped[int]: return mapped_column(ForeignKey("user.myuserid", ondelete="CASCADE")) def get_user_mapping(self) -> dict[str, t.Any]: """ Return the filter needed by find_user() to get the user associated with this webauthn credential. """ return dict(myuserid=self.user_id) class RolesUsers(Base): __tablename__ = "roles_users" id = Column(Integer(), primary_key=True) user_id = Column("user_id", Integer(), ForeignKey("user.myuserid")) role_id = Column("role_id", Integer(), ForeignKey("role.myroleid")) class Role(Base, sqla.FsRoleMixin): __tablename__ = "role" myroleid: Mapped[int] = mapped_column(primary_key=True) # type: ignore id: Mapped[int] = mapped_column(nullable=True) # type: ignore class User(Base, sqla.FsUserMixin): __tablename__ = "user" myuserid: Mapped[int] = mapped_column(primary_key=True) # type: ignore id: Mapped[int] = mapped_column(nullable=True) # type: ignore security_number: Mapped[t.Optional[int]] = mapped_column( # type: ignore unique=True ) def get_security_payload(self): # Make sure we still properly hook up to flask's JSON extension # which handles datetime return {"email": str(self.email), "last_update": self.update_datetime} with app.app_context(): Base.metadata.create_all(bind=engine) def tear_down(): with app.app_context(): Base.metadata.drop_all(bind=engine) engine.dispose() if realdburl: _teardown_realdb(db_info) return SQLAlchemySessionUserDatastore(db_session, User, Role, WebAuthn), tear_down @pytest.fixture() def peewee_datastore(app, tmpdir, realdburl): ds, td = peewee_setup(app, tmpdir, realdburl) yield ds td() def peewee_setup(app, tmpdir, realdburl): pytest.importorskip("peewee") from peewee import ( TextField, DateTimeField, Field, IntegerField, BooleanField, BlobField, ForeignKeyField, CharField, ) from playhouse.flask_utils import FlaskDB if realdburl: engine_mapper = { "postgresql": "peewee.PostgresqlDatabase", "mysql": "peewee.MySQLDatabase", } db_url, db_info = _setup_realdb(realdburl) pieces = urlsplit(db_url) db_config = { "name": pieces.path[1:], "engine": engine_mapper[pieces.scheme.split("+")[0]], "user": pieces.username, "password": pieces.password, "host": pieces.hostname, "port": pieces.port, } else: f, path = tempfile.mkstemp( prefix="flask-security-test-db", suffix=".db", dir=str(tmpdir) ) db_config = {"name": path, "engine": "peewee.SqliteDatabase"} app.config["DATABASE"] = db_config db = FlaskDB(app) class AsaList(Field): field_type = "text" def db_value(self, value): try: return ",".join(value) except TypeError: return value def python_value(self, value): if value: return value.split(",") return [] class Role(RoleMixin, db.Model): name = CharField(unique=True, max_length=80) description = TextField(null=True) permissions = AsaList(null=True) class User(UserMixin, db.Model): email = TextField(unique=True, null=False) fs_uniquifier = TextField(unique=True, null=False) fs_webauthn_user_handle = TextField(unique=True, null=True) username = TextField(unique=True, null=True) security_number = IntegerField(null=True) password = TextField(null=True) last_login_at = DateTimeField(null=True) current_login_at = DateTimeField(null=True) tf_primary_method = TextField(null=True) tf_totp_secret = TextField(null=True) tf_phone_number = TextField(null=True) mf_recovery_codes = AsaList(null=True) us_totp_secrets = TextField(null=True) us_phone_number = TextField(null=True, unique=True) last_login_ip = TextField(null=True) current_login_ip = TextField(null=True) login_count = IntegerField(null=True) active = BooleanField(default=True) confirmed_at = DateTimeField(null=True) def get_security_payload(self): return {"email": str(self.email)} class WebAuthn(WebAuthnMixin, db.Model): credential_id = BlobField(unique=True, null=False, index=True) public_key = BlobField(null=False) sign_count = IntegerField(default=0) transports = AsaList(null=True) # a JSON string as returned from registration extensions = TextField(null=True) lastuse_datetime = DateTimeField(null=False) # name is provided by user - we make sure is unique per user name = TextField(null=False) usage = TextField(null=False) backup_state = BooleanField() device_type = TextField(null=False) # This creates a real column called user_id user = ForeignKeyField(User, backref="webauthn") class UserRoles(db.Model): """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") role = ForeignKeyField(Role, backref="users") 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, WebAuthn): Model.drop_table() Model.create_table() def tear_down(): if realdburl: db.close_db(None) _teardown_realdb(db_info) else: db.close_db(None) os.close(f) os.remove(path) return PeeweeUserDatastore(db, User, Role, UserRoles, WebAuthn), tear_down @pytest.fixture() def pony_datastore(app, tmpdir, realdburl): ds, td = pony_setup(app, tmpdir, realdburl) yield ds td() def pony_setup(app, tmpdir, realdburl): pytest.importorskip("pony") from pony.orm import Database, Optional, Required, Set from pony.orm.core import SetInstance SetInstance.append = SetInstance.add db = Database() class Role(db.Entity): name = Required(str, unique=True) description = Optional(str, nullable=True) users = Set(lambda: User) # type: ignore 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) last_login_at = Optional(datetime) current_login_at = Optional(datetime) tf_primary_method = Optional(str, nullable=True) tf_totp_secret = Optional(str, nullable=True) tf_phone_number = Optional(str, nullable=True) us_totp_secrets = Optional(str, nullable=True) us_phone_number = Optional(str, nullable=True) last_login_ip = Optional(str) current_login_ip = Optional(str) login_count = Optional(int) active = Required(bool, default=True) confirmed_at = Optional(datetime) roles = Set(lambda: Role) def has_role(self, name): return name in {r.name for r in self.roles.copy()} if realdburl: db_url, db_info = _setup_realdb(realdburl) pieces = urlsplit(db_url) provider = pieces.scheme.split("+")[0] provider = "postgres" if provider == "postgresql" else provider db.bind( provider=provider, user=pieces.username, password=pieces.password, host=pieces.hostname, port=pieces.port, database=pieces.path[1:], ) else: app.config["DATABASE"] = {"name": ":memory:", "engine": "pony.SqliteDatabase"} db.bind("sqlite", ":memory:", create_db=True) db.generate_mapping(create_tables=True) def tear_down(): db.disconnect() if realdburl: _teardown_realdb(db_info) return PonyUserDatastore(db, User, Role), tear_down @pytest.fixture() def client(request, app, sqlalchemy_datastore): app.security = Security( app, datastore=sqlalchemy_datastore, **app.fs_constructor_args ) populate_data(app) return app.test_client() @pytest.fixture() def client_nc(request, app, sqlalchemy_datastore): # useful for testing token auth. # No Cookies for You! app.security = Security(app, datastore=sqlalchemy_datastore) populate_data(app) return app.test_client(use_cookies=False) @pytest.fixture( params=[ "cl-fsqlalchemy", "cl-sqla-session", "cl-mongo", "cl-peewee", "cl-fsqlalite", ] ) def clients(request, app, tmpdir, realdburl, realmongodburl): if request.param == "cl-fsqlalchemy": ds, td = sqlalchemy_setup(app, tmpdir, realdburl) elif request.param == "cl-sqla-session": if sys.version_info < (3, 10): pytest.skip("requires python3.10 or higher") ds, td = sqlalchemy_session_setup(app, tmpdir, realdburl) elif request.param == "cl-mongo": ds, td = mongoengine_setup(app, tmpdir, realmongodburl) elif request.param == "cl-peewee": ds, td = peewee_setup(app, tmpdir, realdburl) elif request.param == "cl-pony": # Not working yet. ds, td = pony_setup(app, tmpdir, realdburl) elif request.param == "cl-fsqlalite": ds, td = fsqlalite_setup(app, tmpdir, realdburl) app.security = Security(app, datastore=ds, **app.fs_constructor_args) populate_data(app) if request.param == "cl-peewee": # peewee is insistent on a single connection? ds.db.close_db(None) yield app.test_client() td() @pytest.fixture() def in_app_context(request, app, sqlalchemy_datastore): app.security = Security( app, datastore=sqlalchemy_datastore, **app.fs_constructor_args ) with app.app_context(): yield app @pytest.fixture() def get_message(app: Flask) -> t.Callable[..., bytes]: def fn(key, **kwargs): rv = app.config["SECURITY_MSG_" + key][0] % kwargs return rv.encode("utf-8") 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", "fsqlalite", ] ) def datastore(request, app, tmpdir, realdburl, realmongodburl): if request.param == "sqlalchemy": ds, td = sqlalchemy_setup(app, tmpdir, realdburl) elif request.param == "sqlalchemy-session": ds, td = sqlalchemy_session_setup(app, tmpdir, realdburl) elif request.param == "mongoengine": ds, td = mongoengine_setup(app, tmpdir, realmongodburl) elif request.param == "peewee": ds, td = peewee_setup(app, tmpdir, realdburl) elif request.param == "pony": if sys.version_info >= (3, 13): pytest.skip("pony requires python3.12 or lower") ds, td = pony_setup(app, tmpdir, realdburl) elif request.param == "fsqlalite": ds, td = fsqlalite_setup(app, tmpdir, realdburl) yield ds td() @pytest.fixture() # def script_info(app, datastore): # Fix me when pony works def script_info(app, sqlalchemy_datastore): from flask.cli import ScriptInfo def create_app(): uia = [ {"email": {"mapper": uia_email_mapper}}, {"us_phone_number": {"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) @pytest.fixture() def script_info_min(app, fsqlalite_min_datastore): from flask.cli import ScriptInfo def create_app(): app.security = Security(app, datastore=fsqlalite_min_datastore) return app return ScriptInfo(create_app=create_app) def pytest_addoption(parser): parser.addoption( "--realdburl", action="store", default=None, help="""Set url for using real database for testing. For postgres: 'postgresql://user:password@host/')""", ) parser.addoption( "--realmongodburl", action="store", default=None, help="""Set url for using real mongo database for testing. e.g. 'localhost'""", ) parser.addoption( "--setting", default=None, action="append", help="""Set one or more SECURITY_ settings from command line. e.g. --setting anonymous_user_enable=False""", ) @pytest.fixture(scope="session") def realdburl(request): """ Support running datastore tests against a real DB. For example psycopg2 is very strict about types in queries compared to sqlite To use postgres you need to of course run a postgres instance on localhost then pass in an extra arg to pytest: --realdburl postgresql://@localhost/ For mysql same - just download and add a root password. --realdburl "mysql+pymysql://root:@localhost/" """ return request.config.option.realdburl @pytest.fixture(scope="session") def realmongodburl(request): """ Support running datastore tests against a real Mongo DB. --realmongodburl "localhost" """ return request.config.option.realmongodburl def _setup_realdb(realdburl): """ Called when we want to run unit tests against a real DB. This is useful since different DB drivers are pickier about queries etc. (such as pyscopg2 and postgres) """ from sqlalchemy import create_engine from sqlalchemy_utils import database_exists, create_database db_name = "flask_security_test_%s" % str(time.time()).replace(".", "_") db_uri = realdburl + db_name engine = create_engine(db_uri) if not database_exists(engine.url): create_database(engine.url) print("Setting up real DB at " + db_uri) return db_uri, {"engine": engine} def _teardown_realdb(db_info): from sqlalchemy_utils import drop_database drop_database(db_info["engine"].url) flask-security-5.7.1/tests/templates/000077500000000000000000000000001511046741400176345ustar00rootroot00000000000000flask-security-5.7.1/tests/templates/_messages.html000066400000000000000000000004101511046741400224630ustar00rootroot00000000000000{%- with messages = get_flashed_messages(with_categories=true) -%} {% if messages %}
    {% for category, message in messages %}
  • {{ message }}
  • {% endfor %}
{% endif %} {%- endwith %} flask-security-5.7.1/tests/templates/_nav.html000066400000000000000000000014361511046741400214510ustar00rootroot00000000000000{%- if _fs_is_user_authenticated(current_user) -%}

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

{%- endif %}
  • Index
  • Profile
  • {% if current_user and current_user.has_role('admin') -%}
  • Admin
  • {% endif -%} {% if current_user and (current_user.has_role('admin') or current_user.has_role('editor')) -%}
  • Admin or Editor
  • {% endif -%}
  • {%- if _fs_is_user_authenticated(current_user) -%} Log out {%- else -%} Log in {%- endif -%}
flask-security-5.7.1/tests/templates/custom_security/000077500000000000000000000000001511046741400230755ustar00rootroot00000000000000flask-security-5.7.1/tests/templates/custom_security/change_email.html000066400000000000000000000000531511046741400263550ustar00rootroot00000000000000CUSTOM CHANGE EMAIL {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/change_password.html000066400000000000000000000000561511046741400271330ustar00rootroot00000000000000CUSTOM CHANGE PASSWORD {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/change_username.html000066400000000000000000000000561511046741400271100ustar00rootroot00000000000000CUSTOM CHANGE USERNAME {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/forgot_password.html000066400000000000000000000000561511046741400272060ustar00rootroot00000000000000CUSTOM FORGOT PASSWORD {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/login_user.html000066400000000000000000000000511511046741400261250ustar00rootroot00000000000000CUSTOM LOGIN USER {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/mf_recovery.html000066400000000000000000000000521511046741400263000ustar00rootroot00000000000000CUSTOM MF RECOVERY {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/mf_recovery_codes.html000066400000000000000000000000601511046741400274540ustar00rootroot00000000000000CUSTOM MF RECOVERY CODES {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/recover_username.html000066400000000000000000000000571511046741400273310ustar00rootroot00000000000000CUSTOM RECOVER USERNAME {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/register_user.html000066400000000000000000000000541511046741400266440ustar00rootroot00000000000000CUSTOM REGISTER USER {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/reset_password.html000066400000000000000000000000551511046741400270270ustar00rootroot00000000000000CUSTOM RESET PASSWORD {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/send_confirmation.html000066400000000000000000000000601511046741400274600ustar00rootroot00000000000000CUSTOM SEND CONFIRMATION {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/send_login.html000066400000000000000000000000511511046741400261000ustar00rootroot00000000000000CUSTOM SEND LOGIN {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/tf_setup.html000066400000000000000000000000571511046741400256160ustar00rootroot00000000000000CUSTOM TWO FACTOR SETUP {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/tf_verify.html000066400000000000000000000000651511046741400257610ustar00rootroot00000000000000CUSTOM TWO FACTOR VERIFY CODE {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/us_setup.html000066400000000000000000000000631511046741400256310ustar00rootroot00000000000000CUSTOM UNIFIED SIGNIN SETUP {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/us_signin.html000066400000000000000000000000561511046741400257620ustar00rootroot00000000000000CUSTOM UNIFIED SIGN IN {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/us_verify.html000066400000000000000000000000551511046741400257760ustar00rootroot00000000000000CUSTOM UNIFIED VERIFY {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/verify.html000066400000000000000000000000521511046741400252640ustar00rootroot00000000000000CUSTOM VERIFY USER {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/wan_register.html000066400000000000000000000000531511046741400264520ustar00rootroot00000000000000CUSTOM WAN REGISTER {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/wan_signin.html000066400000000000000000000000511511046741400261130ustar00rootroot00000000000000CUSTOM WAN SIGNIN {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/custom_security/wan_verify.html000066400000000000000000000000511511046741400261300ustar00rootroot00000000000000CUSTOM WAN VERIFY {{ global }} {{ foo }} flask-security-5.7.1/tests/templates/generic_confirm.html000066400000000000000000000004741511046741400236600ustar00rootroot00000000000000{% include "security/_messages.html" %} {% from "security/_macros.html" import render_field_with_errors, render_field, render_form_errors %} {{ render_field_with_errors(send_confirmation_form.email) }} {{ render_field_with_errors(send_confirmation_form.recaptcha) }} {{ render_form_errors(send_confirmation_form) }} flask-security-5.7.1/tests/templates/generic_reset.html000066400000000000000000000004661511046741400233460ustar00rootroot00000000000000{% include "security/_messages.html" %} {% from "security/_macros.html" import render_field_with_errors, render_field, render_form_errors %} {{ render_field_with_errors(forgot_password_form.email) }} {{ render_field_with_errors(forgot_password_form.recaptcha) }} {{ render_form_errors(forgot_password_form) }} flask-security-5.7.1/tests/templates/index.html000066400000000000000000000001161511046741400216270ustar00rootroot00000000000000{% include "_messages.html" %} {% include "_nav.html" %}

{{ content }}

flask-security-5.7.1/tests/templates/register.html000066400000000000000000000010051511046741400223420ustar00rootroot00000000000000{% include "_messages.html" %} {% include "_nav.html" %}

Register

{{ register_user_form.hidden_tag() }} {{ register_user_form.email.label }} {{ register_user_form.email }}
{{ register_user_form.password.label }} {{ register_user_form.password }}
{{ register_user_form.password_confirm.label }} {{ register_user_form.password_confirm }}
{{ register_user_form.submit }}

{{ content }}

flask-security-5.7.1/tests/templates/security/000077500000000000000000000000001511046741400215035ustar00rootroot00000000000000flask-security-5.7.1/tests/templates/security/email/000077500000000000000000000000001511046741400225725ustar00rootroot00000000000000flask-security-5.7.1/tests/templates/security/email/change_email_instructions.txt000066400000000000000000000007301511046741400305530ustar00rootroot00000000000000{# This template receives the following context: link - the link that should be fetched (GET) to confirm token - this token is part of confirmation link - but can be used to construct arbitrary URLs for redirecting. user - the entire user model object security - the Flask-Security configuration #} Link:{{ link }} Email:{{ user.email }} Token:{{ token }} RegisterBlueprint:{{ security.register_blueprint }} Within:{{ config["SECURITY_CHANGE_EMAIL_WITHIN"] }} flask-security-5.7.1/tests/templates/security/email/confirmation_instructions.txt000066400000000000000000000007301511046741400306470ustar00rootroot00000000000000{# This template receives the following context: confirmation_link - the link that should be fetched (GET) to confirm confirmation_token - this token is part of confirmation link - but can be used to construct arbitrary URLs for redirecting. user - the entire user model object security - the Flask-Security configuration #} Link:{{ confirmation_link }} Email:{{ user.email }} Token:{{ confirmation_token }} RegisterBlueprint:{{ security.register_blueprint }} flask-security-5.7.1/tests/templates/security/email/login_instructions.txt000066400000000000000000000001711511046741400272660ustar00rootroot00000000000000Link:{{ login_link }} Email:{{ user.email }} Token:{{ login_token }} RegisterBlueprint:{{ security.register_blueprint }} flask-security-5.7.1/tests/templates/security/email/reset_instructions.txt000066400000000000000000000007751511046741400273120ustar00rootroot00000000000000{# This template receives the following context: reset_link - the link that should be fetched (GET) to reset reset_token - this token is part of reset link - but can be used to construct arbitrary URLs for redirecting. user - the entire user model object security - the Flask-Security configuration #} {{ global }} {{ foo }} Link:{{ reset_link }} Email:{{ user.email }} Token:{{ reset_token }} RegisterBlueprint:{{ security.register_blueprint }} SecurityConfig:{{ config["SECURITY_RESET_URL"] }} flask-security-5.7.1/tests/templates/security/email/us_instructions.txt000066400000000000000000000007411511046741400266100ustar00rootroot00000000000000{# This template receives the following context: login_link - the link that should be fetched (GET) to reset login_token - this token is part of reset link - but can be used to construct arbitrary URLs for redirecting. user - the entire user model object username - username security - the Flask-Security configuration #} Link:{{ login_link }} Email:{{ user.email }} Token:{{ login_token }} RegisterBlueprint:{{ security.register_blueprint }} Username:{{ username }} flask-security-5.7.1/tests/templates/security/email/welcome.txt000066400000000000000000000007571511046741400247770ustar00rootroot00000000000000{# This template receives the following context: confirmation_link - the link that should be fetched (GET) to confirm user - the entire user model object confirmation_token - this token is part of confirmation link - but can be used to construct arbitrary URLs for redirecting. security - the Flask-Security configuration #} Link:{{ confirmation_link }} Email:{{ user.email }} Token:{{ confirmation_token }} RegisterBlueprint:{{ security.register_blueprint }} ExtraContext:{{ foo }} flask-security-5.7.1/tests/templates/security/email/welcome_existing.txt000066400000000000000000000014731511046741400267050ustar00rootroot00000000000000{# This template receives the following context: user - the entire user model object security - the Flask-Security configuration recovery_link - if enabled. reset_link - reset link if enabled reset_token - this token is part of reset link - but can be used to construct arbitrary URLs for redirecting. confirmation_link - confirmation link is user not yet confirmed (and enabled) confirmation_token This template is used when returning generic responses and don't/can't provide detailed errors as part of form validation to avoid email/username enumeration. #} Email:{{ user.email }} User:{{ user.username }} RegisterBlueprint:{{ security.register_blueprint }} ResetLink:{{ reset_link }} ResetToken:{{ reset_token }} ConfirmationLink:{{ confirmation_link }} ConfirmationToken:{{ confirmation_token }} flask-security-5.7.1/tests/test_async.py000066400000000000000000000072071511046741400203720ustar00rootroot00000000000000""" test_async ~~~~~~~~~~ Tests using Flask async. Make sure our decorators allow for async views Make sure signal receivers can be async :copyright: (c) 2023-2023 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import asyncio import base64 import pytest from flask_principal import identity_changed from flask_security import ( anonymous_user_required, auth_token_required, auth_required, http_auth_required, roles_required, roles_accepted, permissions_required, permissions_accepted, unauth_csrf, ) from tests.test_utils import ( authenticate, json_authenticate, ) pytestmark = pytest.mark.flask_async() def test_auth_required(app, client): @app.route("/async_test") @auth_required() async def async_test(): await asyncio.sleep(0) return "Access Granted" authenticate(client) response = client.get("/async_test") assert b"Access Granted" in response.data def test_auth_token_required(app, client): @app.route("/async_test") @auth_token_required async def async_test(): await asyncio.sleep(0) return "Access Granted" @identity_changed.connect_via(app) async def ic(myapp, identity, **extra_args): await asyncio.sleep(0) response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] response = client.get("/async_test?auth_token=" + token) assert b"Access Granted" in response.data def test_auth_http_required(app, client): @app.route("/async_test") @http_auth_required async def async_test(): await asyncio.sleep(0) return "Access Granted" response = client.get( "/async_test", headers={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8") }, ) assert b"Access Granted" in response.data def test_roles_required(app, client): @app.route("/async_test") @roles_required("admin") async def async_test(): await asyncio.sleep(0) return "Access Granted" authenticate(client) response = client.get("/async_test") assert b"Access Granted" in response.data def test_roles_accepted(app, client): @app.route("/async_test") @roles_accepted("admin") async def async_test(): await asyncio.sleep(0) return "Access Granted" authenticate(client) response = client.get("/async_test") assert b"Access Granted" in response.data def test_permissions_required(app, client): @app.route("/async_test") @permissions_required("super") async def async_test(): await asyncio.sleep(0) return "Access Granted" authenticate(client) response = client.get("/async_test") assert b"Access Granted" in response.data def test_permissions_accepted(app, client): @app.route("/async_test") @permissions_accepted("super") async def async_test(): await asyncio.sleep(0) return "Access Granted" authenticate(client) response = client.get("/async_test") assert b"Access Granted" in response.data def test_anon(app, client): @app.route("/async_test") @anonymous_user_required async def async_test(): await asyncio.sleep(0) return "Access Granted" response = client.get("/async_test") assert b"Access Granted" in response.data def test_unauth_csrf(app, client): @app.route("/async_test") @unauth_csrf() async def async_test(): await asyncio.sleep(0) return "Access Granted" response = client.get("/async_test") assert b"Access Granted" in response.data flask-security-5.7.1/tests/test_basic.py000066400000000000000000001314221511046741400203330ustar00rootroot00000000000000""" test_basic ~~~~~~~~~~~ Test common functionality :copyright: (c) 2019-2024 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import base64 from datetime import datetime, timedelta, timezone import json import re import pytest from flask import Blueprint, g from flask_security import uia_email_mapper from flask_security.decorators import auth_required from flask_principal import identity_loaded from freezegun import freeze_time from tests.conftest import v2_param from tests.test_utils import ( authenticate, capture_flashes, capture_queries, check_location, get_auth_token_version_3x, get_form_action, get_form_input_value, hash_password, init_app_with_options, is_authenticated, json_authenticate, logout, populate_data, verify_token, ) def test_login_view(client): response = client.get("/login") assert b"

Login

" in response.data def test_authenticate(client): response = authenticate(client) assert response.status_code == 302 response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data @pytest.mark.settings(anonymous_user_disabled=True) def test_authenticate_no_anon(client): response = authenticate(client) assert response.status_code == 302 response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data def test_authenticate_with_next(client): data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=/page1", data=data, follow_redirects=True) assert b"Page 1" in response.data def test_authenticate_with_next_bp(app, client): api = Blueprint("api", __name__) @api.route("/info") def info(): pass app.register_blueprint(api, url_prefix="/api") data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=api.info", data=data, follow_redirects=False) assert response.status_code == 302 assert "api/info" in response.location def test_authenticate_with_invalid_next(client, get_message): data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http://google.com", data=data) assert get_message("INVALID_REDIRECT") in response.data @pytest.mark.settings(flash_messages=False) def test_authenticate_with_invalid_next_json(client, get_message): data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http://google.com", json=data) assert response.json["response"]["errors"][0].encode() == get_message( "INVALID_REDIRECT" ) def test_authenticate_with_invalid_malformed_next(client, get_message): data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http:///google.com", data=data) assert get_message("INVALID_REDIRECT") in response.data def test_unauthenticated(app, client, get_message): from flask_security import user_unauthenticated from flask import request recvd = [] @user_unauthenticated.connect_via(app) def un(myapp, **extra): assert request.path == "/profile" recvd.append("gotit") response = client.get("/profile", follow_redirects=False) assert len(recvd) == 1 assert response.location == "/login?next=/profile" @pytest.mark.flask_async() def test_unauthenticated_async(app, client, get_message): from flask_security import user_unauthenticated from flask import request recvd = [] @user_unauthenticated.connect_via(app) async def un(myapp, **extra): assert request.path == "/profile" recvd.append("gotit") response = client.get("/profile", follow_redirects=False) assert len(recvd) == 1 assert response.location == "/login?next=/profile" def test_login_template_next(client): # Test that our login template propagates next. response = client.get("/profile", follow_redirects=True) assert "?next=/profile" in response.request.url login_url = get_form_action(response) response = client.post( login_url, data=dict(email="matt@lp.com", password="password"), follow_redirects=True, ) assert b"Profile Page" 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 assert response.location == "http://sub.lp.com" @pytest.mark.settings(subdomain="auth") def test_authenticate_with_root_domain_next(app, client, get_message): # As of Flask 3.1 this must be explicitly set. app.subdomain_matching = True app.config["SERVER_NAME"] = "lp.com" app.config["SECURITY_REDIRECT_ALLOW_SUBDOMAINS"] = True data = dict(email="matt@lp.com", password="password") response = client.post("http://auth.lp.com/login?next=http://lp.com", data=data) assert response.status_code == 302 assert response.location == "http://lp.com" 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 @pytest.mark.settings( redirect_base_domain="bigidea.org", redirect_allowed_subdomains=["my.photo", "blog"] ) def test_allow_subdomains(app, client, get_message): app.config["SERVER_NAME"] = "app.bigidea.org" data = dict(email="matt@lp.com", password="password") # not in subdomain allowed list response = client.post("/login?next=http://blog2.bigidea.org", data=data) assert get_message("INVALID_REDIRECT") in response.data response = client.post("/login?next=http://my.photo.bigidea.org/image", data=data) assert response.location == "http://my.photo.bigidea.org/image" @pytest.mark.settings( redirect_base_domain="bigidea.org", redirect_allowed_subdomains=[] ) def test_redirect_allow_subdomains(app, client, get_message): app.config["SERVER_NAME"] = "bigidea.org" data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http://blog2.bigidea.org", data=data) assert get_message("INVALID_REDIRECT") in response.data response = client.post("/login?next=http://bigidea.org/imin", data=data) assert response.location == "http://bigidea.org/imin" @pytest.mark.settings( post_login_view="http://blog.bigidea.org/post_login", redirect_base_domain="bigidea.org", redirect_allowed_subdomains=["my.photo", "blog"], ) def test_view_redirect(app, client, get_message): app.config["SERVER_NAME"] = "bigidea.org" data = dict(email="matt@lp.com", password="password") response = client.post("/login", data=data) assert response.location == "http://blog.bigidea.org/post_login" 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 def test_authenticate_with_invalid_input(client, get_message): response = client.post( "/login", json=dict(password="password", email="mememe@test.com"), headers={"Content-Type": "application/json"}, ) assert get_message("USER_DOES_NOT_EXIST") in response.data @pytest.mark.settings(post_login_view="/post_login") def test_get_already_authenticated(client): response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data response = client.get("/login", follow_redirects=True) assert b"Post Login" in response.data # should still get extra goodies headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("/login", headers=headers) assert response.status_code == 200 jresponse = response.json["response"] assert all(a in jresponse for a in ["identity_attributes"]) assert "authentication_token" not in jresponse["user"] assert all(a in jresponse["user"] for a in ["email", "last_update"]) @pytest.mark.settings(post_login_view="/post_login") def test_get_already_authenticated_next(client): response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data # This should NOT override post_login_view due to potential redirect loops. response = client.get("/login?next=/page1", follow_redirects=True) assert b"Post Login" in response.data @pytest.mark.settings(post_login_view="/post_login") def test_post_already_authenticated(client): response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data 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"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) @pytest.mark.settings(username_enable=True) def test_login_form_username(client): # If USERNAME_ENABLE is set then login form should have a both an Email and # StringField response = client.get("/login") # Should be both email with Email type and username with autocomplete assert re.search(b']*type="email"[^>]*>', response.data) assert re.search(b']*autocomplete="username"[^>]*>', response.data) assert re.search(b']*autocomplete="current-password"[^>]*>', response.data) @pytest.mark.settings(username_enable=True, username_required=True) def test_login_form_username_required(app, client): # If username required - we should still be able to login with email alone # given default user_identity_attributes response = client.post( "/login", data=dict(email="matt@lp.com", password="password") ) assert check_location(app, response.location, "/") @pytest.mark.confirmable() @pytest.mark.settings( return_generic_responses=True, requires_confirmation_error_view="/confirm" ) def test_generic_response(app, client, get_message): response = client.post( "/login", data=dict(email="mattwho@lp.com", password="forgot") ) assert get_message("GENERIC_AUTHN_FAILED") in response.data response = client.post("/login", data=dict(email="matt@lp.com", password="forgot")) assert get_message("GENERIC_AUTHN_FAILED") in response.data response = client.post( "/login", json=dict(email="mattwho@lp.com", password="forgot") ) # make sure no field error key. assert list(response.json["response"]["field_errors"].keys()) == [""] assert len(response.json["response"]["field_errors"][""]) == 1 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "GENERIC_AUTHN_FAILED" ) response = client.post("/login", json=dict(email="matt@lp.com", password="forgot")) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "GENERIC_AUTHN_FAILED" ) # make sure don't get confirmation required with capture_flashes() as flashes: response = client.post( "/login", data=dict(email="mattwho@lp.com", password="password"), follow_redirects=False, ) assert response.status_code == 200 assert len(flashes) == 0 @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.registerable() @pytest.mark.settings(username_enable=True, return_generic_responses=True) def test_generic_response_username(app, client, get_message): data = dict( email="dude@lp.com", username="dude", password="awesome sunset", password_confirm="awesome sunset", ) response = client.post("/register", json=data) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 200 logout(client) response = client.post( "/login", data=dict( username="dude2", password="awesome sunset", password_confirm="awesome sunset", ), ) assert get_message("GENERIC_AUTHN_FAILED") in response.data response = client.post("/login", json=dict(username="dude2", password="forgot")) # make sure no field error key. assert list(response.json["response"]["field_errors"].keys()) == [""] assert len(response.json["response"]["field_errors"][""]) == 1 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "GENERIC_AUTHN_FAILED" ) def test_unprovided_username(client, get_message): response = authenticate(client, "") assert get_message("USER_DOES_NOT_EXIST") in response.data def test_unprovided_password(client, get_message): response = authenticate(client, password="") assert get_message("PASSWORD_NOT_PROVIDED") in response.data def test_invalid_user(client, get_message): response = authenticate(client, email="bogus@bogus.com") assert get_message("USER_DOES_NOT_EXIST") in response.data def test_bad_password(client, get_message): response = authenticate(client, password="bogus") assert get_message("INVALID_PASSWORD") in response.data def test_inactive_user(client, get_message): response = authenticate(client, "tiya@lp.com", "password") assert get_message("DISABLED_ACCOUNT") in response.data def test_inactive_forbids(app, client, get_message): """Make sure that existing session doesn't work after user marked inactive """ response = authenticate(client, follow_redirects=True) assert response.status_code == 200 # make sure can access restricted page response = client.get("/profile", follow_redirects=True) assert b"Profile Page" in response.data # deactivate matt with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.deactivate_user(user) app.security.datastore.commit() response = client.get("/profile", follow_redirects=False) print(response.data) # should be thrown back to login page. assert response.status_code == 302 assert response.location == "/login?next=/profile" @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 user marked inactive """ response = json_authenticate(client_nc) assert response.status_code == 200 token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": token} # make sure can access restricted page response = client_nc.get("/token", headers=headers) assert b"Token Authentication" in response.data # deactivate matt with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.deactivate_user(user) app.security.datastore.commit() response = client_nc.get("/token", content_type="application/json", headers=headers) 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 get_message("UNAUTHENTICATED")[0] in response.data def test_unset_password(client, get_message): response = authenticate(client, "jess@lp.com", "password") assert get_message("INVALID_PASSWORD") in response.data response = authenticate(client, "jess@lp.com", "") assert get_message("PASSWORD_NOT_PROVIDED") in response.data def test_logout(client): authenticate(client) response = logout(client, follow_redirects=True) assert b"Home Page" in response.data def test_logout_post(client): authenticate(client) response = client.post("/logout", content_type="application/json") assert response.status_code == 200 assert response.json["meta"]["code"] == 200 def test_logout_with_next_invalid(client, get_message): authenticate(client) response = client.get("/logout?next=http://google.com") assert "google.com" not in response.location def test_logout_with_next(client): authenticate(client) response = client.get("/logout?next=/page1", follow_redirects=True) assert b"Page 1" in response.data def test_missing_session_access(client, get_message): response = client.get("/profile", follow_redirects=True) assert get_message("UNAUTHENTICATED") in response.data def test_has_session_access(client): authenticate(client) response = client.get("/profile", follow_redirects=True) assert b"profile" in response.data def test_authorized_access(client): authenticate(client) response = client.get("/admin") assert b"Admin Page" in response.data def test_unauthorized_access(client, get_message): authenticate(client, "joe@lp.com") response = client.get("/admin", follow_redirects=True) assert response.status_code == 403 def test_unauthorized_callable_view(app, sqlalchemy_datastore, get_message): # Test various options using custom unauthorized view def unauthz_view(): from flask import request if request.path == "/admin": return None elif request.path == "/admin_perm": return "" elif request.path == "/admin_and_editor": return "/profile" elif request.path == "/simple": # N.B. security issue - app should verify this is local return request.referrer else: return "not_implemented" app.config["SECURITY_UNAUTHORIZED_VIEW"] = unauthz_view init_app_with_options(app, sqlalchemy_datastore) client = app.test_client() # activate tiya with app.test_request_context("/"): user = app.security.datastore.find_user(email="tiya@lp.com") app.security.datastore.activate_user(user) app.security.datastore.commit() authenticate(client, "tiya@lp.com") assert is_authenticated(client, get_message) response = client.get("/admin") assert response.status_code == 403 response = client.get("/admin_perm") assert response.status_code == 403 response = client.get("/admin_and_editor", follow_redirects=False) assert check_location(app, response.location, "/profile") response = client.get(response.location) assert response.data.count(get_message("UNAUTHORIZED")) == 1 response = client.get( "/simple", headers={"referer": "/myhome"}, follow_redirects=False ) assert check_location(app, response.location, "/myhome") def test_unauthorized_url_view(app, sqlalchemy_datastore): # Test unknown endpoint basically results in redirect to the given string. app.config["SECURITY_UNAUTHORIZED_VIEW"] = ".myendpoint" init_app_with_options(app, sqlalchemy_datastore) client = app.test_client() authenticate(client, "tiya@lp.com") response = client.get("/admin") assert response.status_code == 302 check_location(app, response.location, ".myendpoint") @pytest.mark.settings(unauthorized_view="/unauthz") def test_roles_accepted(clients): # This specifically tests that we can pass a URL for unauthorized_view. for user in ("matt@lp.com", "joe@lp.com"): authenticate(clients, user) response = clients.get("/admin_or_editor") assert b"Admin or Editor Page" in response.data logout(clients) 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(clients): for user in ("matt@lp.com", "joe@lp.com"): authenticate(clients, user) response = clients.get("/admin_perm") assert b"Admin Page with full-write or super" in response.data logout(clients) 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(clients): for user in ["matt@lp.com"]: authenticate(clients, user) response = clients.get("/admin_perm_required") assert b"Admin Page required" in response.data logout(clients) authenticate(clients, "joe@lp.com") response = clients.get("/admin_perm_required", follow_redirects=True) assert b"Unauthorized" in response.data @pytest.mark.settings(unauthorized_view="unauthz") def test_unauthenticated_role_required(client, get_message): response = client.get("/admin", follow_redirects=True) assert get_message("UNAUTHORIZED") in response.data @pytest.mark.settings(unauthorized_view="unauthz") def test_multiple_role_required(clients): for user in ("matt@lp.com", "joe@lp.com"): authenticate(clients, user) response = clients.get("/admin_and_editor", follow_redirects=True) assert b"Unauthorized" in response.data clients.get("/logout") authenticate(clients, "dave@lp.com") response = clients.get("/admin_and_editor", follow_redirects=True) assert b"Admin and Editor Page" in response.data def test_ok_json_auth(client): response = json_authenticate(client) assert response.json["meta"]["code"] == 200 assert "authentication_token" in response.json["response"]["user"] def test_invalid_json_auth(client): response = json_authenticate(client, password="junk") assert b'"code": 400' in response.data def test_token_auth_via_querystring_valid_token(client): response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] response = client.get("/token?auth_token=" + token) assert b"Token Authentication" in response.data def test_token_auth_via_header_valid_token(client): response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": token} response = client.get("/token", headers=headers) assert b"Token Authentication" in response.data def test_token_auth_via_querystring_invalid_token(client): response = client.get("/token?auth_token=X", headers={"Accept": "application/json"}) assert response.status_code == 401 def test_token_auth_via_header_invalid_token(client): response = client.get( "/token", headers={"Authentication-Token": "X", "Accept": "application/json"} ) assert response.status_code == 401 def test_token_auth_invalid_for_session_auth(client): # when user is loaded from token data, session authentication should fail. response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] # logout so session doesn't contain valid user details logout(client) headers = {"Authentication-Token": token, "Accept": "application/json"} response = client.get("/session", headers=headers) assert response.status_code == 401 def test_per_user_expired_token(app, client_nc): # Test expiry in auth_token using callable with freeze_time("2024-01-01"): def exp(user): assert user.email == "matt@lp.com" return int((datetime.now(timezone.utc) + timedelta(days=1)).timestamp()) app.config["SECURITY_TOKEN_EXPIRE_TIMESTAMP"] = exp response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] verify_token(client_nc, token, status=401) def test_per_user_not_expired_token(app, client_nc): # Test expiry in auth_token using callable def exp(user): assert user.email == "matt@lp.com" return int((datetime.now(timezone.utc) + timedelta(days=1)).timestamp()) app.config["SECURITY_TOKEN_EXPIRE_TIMESTAMP"] = exp response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] verify_token(client_nc, token) def test_garbled_auth_token(app, client_nc): # garble token def augment_auth_token(tdata): del tdata["exp"] app.config["TESTING_AUGMENT_AUTH_TOKEN"] = augment_auth_token response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] verify_token(client_nc, token, status=401) @pytest.mark.csrf(ignore_unauth=True, csrfprotect=True) def test_token_auth_csrf(client): response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] csrf_token = response.json["response"]["csrf_token"] headers = {"Authentication-Token": token} response = client.post("/token", headers=headers) assert b"The CSRF token is missing" in response.data # test JSON version response = client.post("/token", headers=headers, content_type="application/json") assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." # now do it right headers["X-CSRF-Token"] = csrf_token response = client.post( "/token", headers=headers, ) assert b"Token Authentication" in response.data def test_http_auth(client, get_message): # 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 get_message("UNAUTHENTICATED") 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={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8") }, ) assert b"HTTP Authentication" in response.data @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", headers={ "Authorization": "Basic %s" % base64.b64encode(b"jill:password").decode("utf-8") }, ) assert b"HTTP Authentication" in response.data def test_http_auth_no_authorization(client): response = client.get( "/http_admin_required", headers={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8") }, ) assert response.status_code == 403 def test_http_auth_no_authorization_json(client, get_message): response = client.get( "/http_admin_required", headers={ "accept": "application/json", "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8"), }, ) assert response.status_code == 403 assert response.headers["Content-Type"] == "application/json" def test_http_auth_no_authentication(client, get_message): response = client.get("/http", headers={}) assert response.status_code == 401 assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] def test_http_auth_no_authentication_json(client, get_message): response = client.get("/http", headers={"accept": "application/json"}) assert response.status_code == 401 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "UNAUTHENTICATED" ) assert response.headers["Content-Type"] == "application/json" def test_invalid_http_auth_invalid_username(client, get_message): response = client.get( "/http", headers={ "Authorization": "Basic %s" % base64.b64encode(b"bogus:bogus").decode("utf-8") }, ) assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] def test_invalid_http_auth_invalid_username_json(client, get_message): # Even with JSON - Basic Auth required a WWW-Authenticate header response. response = client.get( "/http", headers={ "accept": "application/json", "Authorization": "Basic %s" % base64.b64encode(b"bogus:bogus").decode("utf-8"), }, ) assert response.status_code == 401 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "UNAUTHENTICATED" ) assert response.headers["Content-Type"] == "application/json" assert "WWW-Authenticate" in response.headers def test_invalid_http_auth_bad_password(client, get_message): response = client.get( "/http", headers={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:bogus").decode("utf-8") }, ) assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] def test_custom_http_auth_realm(client, get_message): response = client.get( "/http_custom_realm", headers={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:bogus").decode("utf-8") }, ) assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="My Realm"' == response.headers["WWW-Authenticate"] @pytest.mark.csrf(csrfprotect=True) def test_http_auth_csrf(client, get_message): headers = { "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8") } response = client.post( "/http", headers=headers, ) assert b"The CSRF token is missing" in response.data # test JSON version response = client.post("/http", headers=headers, content_type="application/json") assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." # grab a csrf_token response = client.get("/login") csrf_token = get_form_input_value(response, "csrf_token") headers["X-CSRF-Token"] = csrf_token response = client.post( "/http", headers=headers, ) assert b"HTTP Authentication" in response.data def test_multi_auth_basic(client): response = client.get( "/multi_auth", headers={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8") }, ) assert b"Basic" in response.data response = client.get("/multi_auth") # 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 def test_multi_auth_basic_invalid(client, get_message): response = client.get( "/multi_auth", headers={ "Authorization": "Basic %s" % base64.b64encode(b"bogus:bogus").decode("utf-8") }, ) assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] response = client.get("/multi_auth") assert response.status_code == 401 def test_multi_auth_token(client): response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] response = client.get("/multi_auth?auth_token=" + token) assert b"Token" in response.data def test_multi_auth_session(client): authenticate(client) response = client.get("/multi_auth") 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) with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.delete_user(user) app.security.datastore.commit() response = client.get("/") assert b"Hello matt@lp.com" not in response.data def test_session_loads_identity(app, client): @app.route("/identity_check") @auth_required("session") def id_check(): if hasattr(g, "identity"): identity = g.identity assert hasattr(identity, "loader_called") assert identity.loader_called return "Success" json_authenticate(client) # add identity loader after authentication to only fire it for # session-authentication next `get` call @identity_loaded.connect_via(app) def identity_loaded_check(sender, identity): identity.loader_called = True response = client.get("/identity_check") assert b"Success" == response.data def test_remember_token(client): response = authenticate(client, follow_redirects=False) client.delete_cookie("session") response = client.get("/profile") assert b"profile" in response.data def test_request_loader_does_not_fail_with_invalid_token(client): client.set_cookie("remember_token") response = client.get("/") assert b"BadSignature" not in response.data def test_sending_auth_token_with_json(client): response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] data = '{"auth_token": "%s"}' % token response = client.post( "/token", data=data, headers={"Content-Type": "application/json"} ) assert b"Token Authentication" in response.data def test_json_not_dict(client): response = client.post( "/json", data=json.dumps(["thing1", "thing2"]), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 def test_login_info(client): # Make sure we can get user info when logged in already. json_authenticate(client) response = client.get("/login", headers={"Content-Type": "application/json"}) assert response.status_code == 200 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"]["email"] == "matt@lp.com" assert "last_update" in response.json["response"]["user"] @pytest.mark.registerable() @pytest.mark.settings(post_login_view="/anon_required") def test_anon_required(client, get_message): """If logged in, should get 'anonymous_user_required' redirect""" response = authenticate(client, follow_redirects=False) response = client.get("/register") assert "location" in response.headers assert "/anon_required" in response.location @pytest.mark.registerable() @pytest.mark.settings(post_login_view="/anon_required") def test_anon_required_json(client, get_message): """If logged in, should get 'anonymous_user_required' response""" authenticate(client, follow_redirects=False) response = client.get("/register", headers={"Accept": "application/json"}) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "ANONYMOUS_USER_REQUIRED" ) def test_change_uniquifier(app, client_nc): # make sure that existing token no longer works once we change the uniquifier 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_verifying_token_from_version_3x(app, client): """ Check token generated with flask security 3.x, which has different form than token from version 4.0.0, can be verified """ from .test_utils import get_auth_token_version_3x with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") token = get_auth_token_version_3x(app, user) headers = {"Authentication-Token": token, "Accept": "application/json"} response = client.get("/profile", headers=headers) assert response.status_code == 200 def test_verifying_token_from_version_4x(app, client): from .test_utils import get_auth_token_version_4x with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") token = get_auth_token_version_4x(app, user) headers = {"Authentication-Token": token, "Accept": "application/json"} response = client.get("/profile", headers=headers) assert response.status_code == 200 def test_change_token_uniquifier(app): pytest.importorskip("sqlalchemy") pytest.importorskip("flask_sqlalchemy") # 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) with app.app_context(): db.engine.dispose() def test_null_token_uniquifier(app): pytest.importorskip("sqlalchemy") pytest.importorskip("flask_sqlalchemy") # 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) with app.app_context(): db.engine.dispose() def test_token_query(app, client_nc): # Verify that when authenticating with auth token (and not session) # that there is just one DB query to get user. with capture_queries(app.security.datastore) as queries: response = json_authenticate(client_nc) assert len(queries) == 1 token = response.json["response"]["user"]["authentication_token"] response = client_nc.get( "/token", headers={"Content-Type": "application/json", "Authentication-Token": token}, ) assert response.status_code == 200 assert len(queries) == 2 def test_session_query(in_app_context): # Verify that when authenticating with auth token (but also sending session) # that there are 2 DB queries to get user. # This is since the session will load one - but auth_token_required needs to # verify that the TOKEN is valid (and it is possible that the user_id in the # session is different that the one in the token (huh?) myapp = in_app_context populate_data(myapp) myclient = myapp.test_client() response = json_authenticate(myclient) token = response.json["response"]["user"]["authentication_token"] with capture_queries(myapp.security.datastore) as queries: response = myclient.get( "/token", headers={"Content-Type": "application/json", "Authentication-Token": token}, ) assert response.status_code == 200 assert len(queries) == 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"] def test_auth_token_decorator(app, client_nc): """ Test accessing endpoint decorated with auth_token_required when using token generated by flask security 3.x algorithm """ with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") token = get_auth_token_version_3x(app, user) response = client_nc.get( "/token", headers={"Content-Type": "application/json", "Authentication-Token": token}, ) assert response.status_code == 200 flask-security-5.7.1/tests/test_change_email.py000066400000000000000000000204121511046741400216420ustar00rootroot00000000000000""" test_change_email ~~~~~~~~~~~~~~~~~ Change email functionality tests :copyright: (c) 2024-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from contextlib import contextmanager from datetime import date, timedelta import re from urllib.parse import urlsplit import pytest from freezegun import freeze_time from tests.test_utils import ( authenticate, capture_flashes, is_authenticated, json_authenticate, logout, ) from flask_security import hash_password from flask_security.signals import ( change_email_instructions_sent, change_email_confirmed, ) pytestmark = pytest.mark.change_email() @contextmanager def capture_change_email_requests(): change_email_requests = [] def _on(app, **data): change_email_requests.append(data) change_email_instructions_sent.connect(_on) try: yield change_email_requests finally: change_email_instructions_sent.disconnect(_on) @pytest.mark.settings(change_email_error_view="/change-email") def test_ce(app, clients, get_message, outbox): client = clients @change_email_confirmed.connect_via(app) def _on(app, **kwargs): assert kwargs["old_email"] == "matt@lp.com" assert kwargs["user"].email == "matt2@lp.com" authenticate(client, email="matt@lp.com") with capture_change_email_requests() as ce_requests: response = client.post("/change-email", data={"email": ""}) assert get_message("INVALID_EMAIL_ADDRESS") in response.data assert len(outbox) == 0 response = client.post("/change-email", data=dict(email="matt2@lp.com")) msg = get_message("CHANGE_EMAIL_SENT", email="matt2@lp.com") assert msg in response.data assert "matt2@lp.com" == ce_requests[0]["new_email"] token = ce_requests[0]["token"] assert len(outbox) == 1 assert app.config["SECURITY_CHANGE_EMAIL_WITHIN"] in outbox[0].body response = client.get("/change-email/" + token, follow_redirects=True) assert get_message("CHANGE_EMAIL_CONFIRMED") in response.data assert is_authenticated(client, get_message) logout(client) authenticate(client, email="matt2@lp.com") assert is_authenticated(client, get_message) # try using link again - should fail with capture_flashes() as flashes: client.get("/change-email/" + token, follow_redirects=True) assert flashes[0]["message"].encode("utf-8") == get_message("API_ERROR") def test_ce_json(app, client, get_message, outbox): headers = {"Accept": "application/json", "Content-Type": "application/json"} @change_email_confirmed.connect_via(app) def _on(app, **kwargs): assert kwargs["old_email"] == "matt@lp.com" assert kwargs["user"].email == "matt2@lp.com" json_authenticate(client, email="matt@lp.com") with capture_change_email_requests() as ce_requests: response = client.post("/change-email", json={"email": ""}) assert response.json["response"]["errors"][0].encode("utf=8") == get_message( "INVALID_EMAIL_ADDRESS" ) assert len(outbox) == 0 response = client.post("/change-email", json=dict(email="matt2@lp.com")) assert response.status_code == 200 assert response.json["response"]["current_email"] == "matt@lp.com" assert "matt2@lp.com" == ce_requests[0]["new_email"] token = ce_requests[0]["token"] assert len(outbox) == 1 response = client.get( "/change-email/" + token, headers=headers, follow_redirects=True ) assert get_message("CHANGE_EMAIL_CONFIRMED") in response.data assert is_authenticated(client, get_message) logout(client) authenticate(client, email="matt2@lp.com") assert is_authenticated(client, get_message) @pytest.mark.settings( change_email_within="1 milliseconds", change_email_error_view="/change-email" ) def test_expired_token(client, get_message): # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): authenticate(client, email="matt@lp.com") with capture_change_email_requests() as ce_requests: client.post("/change-email", data=dict(email="matt2@lp.com")) assert "matt2@lp.com" == ce_requests[0]["new_email"] token = ce_requests[0]["token"] response = client.get("/change-email/" + token, follow_redirects=True) msg = get_message("CHANGE_EMAIL_EXPIRED", within="1 milliseconds") assert msg in response.data def test_template(app, client, get_message, outbox): # Check contents of email template - this uses a test template # in order to check all context vars since the default template # doesn't have all of them. authenticate(client, email="matt@lp.com") with capture_change_email_requests() as ce_requests: client.post("/change-email", data=dict(email="matt2@lp.com")) # check email assert outbox[0].recipients[0] == "matt2@lp.com" matcher = re.findall(r"\w+:.*", outbox[0].body, re.IGNORECASE) # should be 4 - link, email, token, config item assert matcher[1].split(":")[1] == "matt@lp.com" assert matcher[2].split(":")[1] == ce_requests[0]["token"] assert matcher[3].split(":")[1] == "True" # register_blueprint assert matcher[4].split(":")[1] == "2 hours" # check link _, link = matcher[0].split(":", 1) response = client.get(link, follow_redirects=True) assert get_message("CHANGE_EMAIL_CONFIRMED") in response.data @pytest.mark.settings(return_generic_responses=True) def test_generic_response(app, client, get_message, outbox): authenticate(client, email="matt@lp.com") with capture_change_email_requests(): # first try bad formatted email - should get detailed error response = client.post("/change-email", json={"email": ""}) assert response.json["response"]["errors"][0].encode("utf=8") == get_message( "INVALID_EMAIL_ADDRESS" ) # try existing email - should get same response as if it 'worked' response = client.post("/change-email", data=dict(email="gal@lp.com")) msg = get_message("CHANGE_EMAIL_SENT", email="gal@lp.com") assert msg in response.data # but no email was actually sent assert len(outbox) == 0 @pytest.mark.settings( redirect_host="myui.com:8090", redirect_behavior="spa", post_change_email_view="/change-email-redirect", change_email_error_view="/change-email-error", ) def test_spa_get(app, client, get_message): json_authenticate(client, email="matt@lp.com") with capture_change_email_requests() as ce_requests: response = client.post("/change-email", data=dict(email="matt2@lp.com")) msg = get_message("CHANGE_EMAIL_SENT", email="matt2@lp.com") assert msg in response.data assert "matt2@lp.com" == ce_requests[0]["new_email"] token = ce_requests[0]["token"] response = client.get("/change-email/" + token, follow_redirects=False) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "myui.com:8090" == split.netloc assert "/change-email-redirect" == split.path # again - should be an error response = client.get("/change-email/" + token, follow_redirects=False) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "myui.com:8090" == split.netloc assert "/change-email-error" == split.path @pytest.mark.settings(change_email_error_view="/change-email") def test_ce_race(app, client, get_message): # test that if an email is taken between the link being sent and # the user confirming - they get an error authenticate(client, email="matt@lp.com") with capture_change_email_requests() as ce_requests: client.post("/change-email", data=dict(email="matt2@lp.com")) token = ce_requests[0]["token"] with app.app_context(): app.security.datastore.create_user( email="matt2@lp.com", password=hash_password("password"), ) app.security.datastore.commit() with capture_flashes() as flashes: client.get("/change-email/" + token, follow_redirects=True) assert flashes[0]["message"].encode("utf-8") == get_message("API_ERROR") flask-security-5.7.1/tests/test_change_username.py000066400000000000000000000265511511046741400224040ustar00rootroot00000000000000""" test_change_username ~~~~~~~~~~~~~~~~~~~~ Change username tests :copyright: (c) 2025-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import pytest from flask import Flask import markupsafe from flask_security import UsernameUtil, UserMixin, username_changed from flask_security.forms import _default_field_labels from flask_security.utils import localize_callback from tests.test_utils import ( authenticate, check_location, check_xlation, get_form_input_value, init_app_with_options, is_authenticated, logout, ) pytestmark = pytest.mark.change_username() @pytest.mark.settings( post_change_username_view="/post_change_username", username_enable=True ) def test_cu(app, clients, get_message, outbox): recorded = [] @username_changed.connect_via(app) def on_username_changed(app, user, old_username): assert isinstance(app, Flask) assert isinstance(user, UserMixin) recorded.append((user, old_username)) authenticate(clients) # Test change view response = clients.get("/change-username", follow_redirects=True) assert b"Change Username" in response.data # test length validation response = clients.post( "/change-username", data={"username": "me"}, follow_redirects=True, ) assert get_message("USERNAME_INVALID_LENGTH", min=4, max=32) in response.data # Test successful submit sends email notification response = clients.post( "/change-username", data={"username": "memynewusername"}, follow_redirects=True, ) assert get_message("USERNAME_CHANGE") in response.data assert b"Post Change Username" in response.data assert len(recorded) == 1 assert len(outbox) == 1 assert "Your username has been changed" in outbox[0].body response = clients.get("/change-username", follow_redirects=True) assert b"memynewusername" in response.data # authenticate with new username logout(clients) clients.post("/login", data=dict(username="memynewusername", password="password")) assert is_authenticated(clients, get_message) # Test same as previous response = clients.post( "/change-username", data={"username": "memynewusername"}, follow_redirects=True, ) assert ( get_message("USERNAME_ALREADY_ASSOCIATED", username="memynewusername") in response.data ) # since username isn't required - change it to nothing response = clients.post( "/change-username", data={"username": ""}, follow_redirects=True, ) assert get_message("USERNAME_CHANGE") in response.data assert b"Post Change Username" in response.data assert len(recorded) == 2 assert len(outbox) == 2 assert "Your username has been changed" in outbox[1].body # shouldn't be able to log in with username logout(clients) clients.post("/login", data=dict(username="memynewusername", password="password")) assert not is_authenticated(clients, get_message) @pytest.mark.settings( post_change_username_view="/post_change_username", username_enable=True ) def test_cu_json(app, clients, get_message): # Test JSON recorded = [] @username_changed.connect_via(app) def on_username_changed(app, user, old_username): recorded.append((user, old_username)) response = clients.get("/change-username", content_type="application/json") assert response.status_code == 401 authenticate(clients) response = clients.get("/change-username", content_type="application/json") assert response.json["response"]["current_username"] == "matt" response = clients.post("/change-username", json={"username": "memyjsonusername"}) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/json" assert len(recorded) == 1 user, old = recorded[0] assert old == "matt" # Test JSON errors response = clients.post("/change-username", json={"username": "my"}) assert response.status_code == 400 assert response.json["response"]["field_errors"]["username"] == [ "Username must be at least 4 characters and less than 32 characters" ] # authenticate with old username logout(clients) clients.post("/login", json=dict(username="matt", password="password")) assert not is_authenticated(clients, get_message) # authenticate with new username logout(clients) clients.post("/login", json=dict(username="memyjsonusername", password="password")) assert is_authenticated(clients, get_message) @pytest.mark.settings(username_enable=True, username_required=True) def test_cu_required(app, client, get_message): client.post("/login", json=dict(username="matt", password="password")) response = client.post("/change-username", json={"username": ""}) assert ( get_message("USERNAME_NOT_PROVIDED") == response.json["response"]["field_errors"]["username"][0].encode() ) @pytest.mark.app_settings(babel_default_locale="fr_FR", SECURITY_CHANGEABLE=True) @pytest.mark.babel() def test_xlation(app, client, get_message_local, outbox): # Test form and email translation 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.test_request_context(): # Check header assert b"

Changer le mot de passe

" in response.data submit = localize_callback(_default_field_labels["change_password"]) assert f'value="{submit}"'.encode() in response.data response = client.post( "/change", data={ "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", }, follow_redirects=True, ) with app.test_request_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(markupsafe.escape(localize_callback("Your password has been changed."))) in outbox[0].alts["html"] ) assert localize_callback("Your password has been changed") in outbox[0].body @pytest.mark.settings(change_username_url="/custom-change-username") def test_custom_change_url(client): authenticate(client) response = client.get("/custom-change-username") assert response.status_code == 200 assert b"Change Username" in response.data @pytest.mark.settings(change_username_template="custom_security/change_username.html") def test_custom_change_template(client): authenticate(client) response = client.get("/change-username") assert b"CUSTOM CHANGE USERNAME" in response.data @pytest.mark.settings(send_username_change_email=False) def test_disable_change_emails(app, client, outbox): authenticate(client) response = client.post( "/change-username", json={"username": "mynewusername"}, follow_redirects=True, ) assert response.status_code == 200 assert len(outbox) == 0 @pytest.mark.settings(post_change_username_view="/profile") def test_custom_post_change_view(client): authenticate(client) response = client.post( "/change-username", data={"username": "mynewusername"}, follow_redirects=True, ) assert b"Profile Page" in response.data def test_my_validator(app, sqlalchemy_datastore): class MyUsernameUtil(UsernameUtil): def check_username(self, username): if username == "nowayjose": return "Are you crazy?" init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"username_util_cls": MyUsernameUtil}}, ) tcl = app.test_client() authenticate(tcl) response = tcl.post("/change-username", json=dict(username="nowayjose")) assert response.status_code == 400 assert "Are you crazy" in response.json["response"]["errors"][0] @pytest.mark.settings(username_enable=True) def test_username_normalize(app, client): """Verify that can log in with both original and normalized username""" authenticate(client) response = client.post( "/change-username", json=dict(username="newusername\N{ROMAN NUMERAL ONE}") ) assert response.status_code == 200 logout(client) # use original typed-in username response = client.post( "/login", json=dict(username="newusername\N{ROMAN NUMERAL ONE}", password="password"), ) assert response.status_code == 200 logout(client) # try with normalized username response = client.post( "/login", json=dict( username="newusername\N{LATIN CAPITAL LETTER I}", password="password", ), ) assert response.status_code == 200 @pytest.mark.settings(username_normalize_form=None, username_enable=True) def test_username_no_normalize(app, client): """Verify that can log in with original but not normalized if have disabled normalization """ authenticate(client) response = client.post( "/change-username", json=dict(username="newusername\N{ROMAN NUMERAL ONE}") ) assert response.status_code == 200 logout(client) # try with normalized password - should fail response = client.post( "/login", json=dict( username="newusername\N{LATIN CAPITAL LETTER I}", password="password" ), ) assert response.status_code == 400 # use original typed-in username response = client.post( "/login", json=dict(username="newusername\N{ROMAN NUMERAL ONE}", password="password"), ) assert response.status_code == 200 @pytest.mark.csrf(ignore_unauth=True) @pytest.mark.settings(post_change_username_view="/post_change_username_view") def test_csrf(app, client): # enable CSRF, make sure template shows CSRF errors. authenticate(client) data = { "username": "mynewusername", } response = client.post("/change-username", data=data) assert b"The CSRF token is missing" in response.data # Note that we get a CSRF token EVEN for errors - this seems odd # but can't find anything that says its a security issue csrf_token = get_form_input_value(response, "csrf_token") data["csrf_token"] = csrf_token response = client.post("/change-username", data=data) assert check_location(app, response.location, "/post_change_username_view") @pytest.mark.csrf(ignore_unauth=True, csrfprotect=True) def test_csrf_json(app, client): # This tests the handle_csrf code path - especially the JSON code path # that should return a JSON response! authenticate(client) response = client.post("/change-username", json=dict(username="mynewusername")) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." response = client.get("/change-username", content_type="application/json") csrf_token = response.json["response"]["csrf_token"] response = client.post( "/change-username", json=dict(username="mynewusername"), headers={"X-CSRF-Token": csrf_token}, ) assert response.status_code == 200 flask-security-5.7.1/tests/test_changeable.py000066400000000000000000000545201511046741400213260ustar00rootroot00000000000000""" test_changeable ~~~~~~~~~~~~~~~ Changeable tests :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import base64 import pytest from flask import Flask import markupsafe from flask_security import PasswordUtil, UserMixin, password_changed, user_authenticated from flask_security.forms import _default_field_labels from flask_security.utils import localize_callback from tests.test_utils import ( authenticate, check_location, check_xlation, get_form_input_value, get_session, hash_password, init_app_with_options, json_authenticate, logout, ) pytestmark = pytest.mark.changeable() def test_changeable_flag(app, clients, get_message, outbox): tcl = clients recorded = [] @password_changed.connect_via(app) def on_password_changed(app, user): assert isinstance(app, Flask) assert isinstance(user, UserMixin) recorded.append(user) authenticate(tcl) # Test change view response = tcl.get("/change", follow_redirects=True) assert b"Change Password" in response.data # Test wrong original password response = tcl.post( "/change", data={ "password": "notpassword", "new_password": "newpassword", "new_password_confirm": "newpassword", }, follow_redirects=True, ) assert get_message("INVALID_PASSWORD") in response.data # Test mismatch response = tcl.post( "/change", data={ "password": "password", "new_password": "newpassword", "new_password_confirm": "notnewpassword", }, follow_redirects=True, ) assert get_message("RETYPE_PASSWORD_MISMATCH") in response.data # Test missing password response = tcl.post( "/change", data={"password": " ", "new_password": "", "new_password_confirm": ""}, follow_redirects=True, ) assert get_message("PASSWORD_NOT_PROVIDED") in response.data response = tcl.post( "/change", data={ "password": " ", "new_password": "awesome password", "new_password_confirm": "awesome password", }, follow_redirects=True, ) assert get_message("PASSWORD_NOT_PROVIDED") in response.data # Test bad password response = tcl.post( "/change", data={"password": "password", "new_password": "a", "new_password_confirm": "a"}, follow_redirects=True, ) assert get_message("PASSWORD_INVALID_LENGTH", length=8) in response.data # Test same as previous response = tcl.post( "/change", data={ "password": "password", "new_password": "password", "new_password_confirm": "password", }, follow_redirects=True, ) assert get_message("PASSWORD_IS_THE_SAME") in response.data # Test successful submit sends email notification response = tcl.post( "/change", data={ "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", }, follow_redirects=True, ) assert get_message("PASSWORD_CHANGE") in response.data assert b"Home Page" in response.data assert len(recorded) == 1 assert len(outbox) == 1 assert "Your password has been changed" in outbox[0].body # Test leading & trailing whitespace not stripped response = tcl.post( "/change", data={ "password": "new strong password", "new_password": " new strong password ", "new_password_confirm": " new strong password ", }, follow_redirects=True, ) assert get_message("PASSWORD_CHANGE") in response.data # Test JSON data = ( '{"password": " new strong password ", ' '"new_password": "new stronger password2", ' '"new_password_confirm": "new stronger password2"}' ) response = tcl.post( "/change", data=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/json" # Test JSON errors data = '{"password": "newpassword"}' response = tcl.post( "/change", data=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert response.json["response"]["field_errors"]["new_password"] == [ "Password not provided" ] 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.location == "/login?next=/profile" 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 client.get_cookie("remember_token") client.delete_cookie("session") 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.location == "/login?next=/token" def test_auth_uniquifier(app): pytest.importorskip("sqlalchemy") pytest.importorskip("flask_sqlalchemy") # 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 with app.app_context(): db.engine.dispose() @pytest.mark.app_settings(babel_default_locale="fr_FR") @pytest.mark.babel() def test_xlation(app, client, get_message_local, outbox): # Test form and email translation 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.test_request_context(): # Check header assert ( f'

{localize_callback("Change Password")}

'.encode() in response.data ) submit = localize_callback(_default_field_labels["change_password"]) assert f'value="{submit}"'.encode() in response.data response = client.post( "/change", data={ "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", }, follow_redirects=True, ) with app.test_request_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(markupsafe.escape(localize_callback("Your password has been changed."))) in outbox[0].alts["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) response = client.get("/custom_change") assert response.status_code == 200 @pytest.mark.settings(change_password_template="custom_security/change_password.html") def test_custom_change_template(client): authenticate(client) response = client.get("/change") assert b"CUSTOM CHANGE PASSWORD" in response.data @pytest.mark.settings(send_password_change_email=False) def test_disable_change_emails(app, client, outbox): client.post( "/change", data={ "password": "password", "new_password": "newpassword", "new_password_confirm": "newpassword", }, follow_redirects=True, ) assert len(outbox) == 0 @pytest.mark.settings(post_change_view="/profile") def test_custom_post_change_view(client): authenticate(client) response = client.post( "/change", data={ "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", }, follow_redirects=True, ) assert b"Profile Page" in response.data def test_token_change(app, client_nc): # Verify can change password using token auth only login_response = json_authenticate(client_nc) token = login_response.json["response"]["user"]["authentication_token"] data = dict( password="password", new_password="new strong password", new_password_confirm="new strong password", ) response = client_nc.post( "/change?include_auth_token=1", json=data, headers={"Content-Type": "application/json", "Authentication-Token": token}, ) assert response.status_code == 200 assert "authentication_token" in response.json["response"]["user"] @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 get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers response = client_nc.post( "/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 # No session so no flashing assert b"Home Page" in response.data def __test_easy_password(client): authenticate(client) data = ( '{"password": "password", ' '"new_password": "mattmatt2", ' '"new_password_confirm": "mattmatt2"}' ) response = client.post( "/change", data=data, headers={"Content-Type": "application/json"} ) assert response.headers["Content-Type"] == "application/json" return response @pytest.mark.settings(password_complexity_checker="zxcvbn") def test_easy_password(app, client): response = __test_easy_password(client) assert response.status_code == 400 # Response from zxcvbn assert "Repeats like" in response.json["response"]["errors"][0] @pytest.mark.settings(password_complexity_checker="zxcvbn", zxcvbn_minimum_score=0) def test_easy_password_ok(app, client): response = __test_easy_password(client) assert response.status_code == 200 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) data = ( '{"password": "password", ' '"new_password": "mattmatt2", ' '"new_password_confirm": "mattmatt2"}' ) response = client.post( "/change", data=data, headers={"Content-Type": "application/json"} ) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 400 assert "Are you crazy" in response.json["response"]["errors"][0] @pytest.mark.settings(password_length_min=12) def test_override_length(app, client, get_message): authenticate(client) response = client.post( "/change", data={ "password": "password", "new_password": "01234567890", "new_password_confirm": "01234567890", }, follow_redirects=True, ) assert get_message("PASSWORD_INVALID_LENGTH", length=12) in response.data def test_unicode_length(app, client, get_message): # From NIST and OWASP - each unicode code point should count as a character. authenticate(client) # Emoji's are 4 bytes in utf-8 data = dict( password="password", new_password="\N{CYCLONE}\N{SUNRISE}\N{FOGGY}" "\N{VOLCANO}\N{CRESCENT MOON}\N{MILKY WAY}" "\N{FOG}\N{THERMOMETER}\N{ROSE}", new_password_confirm="\N{CYCLONE}\N{SUNRISE}\N{FOGGY}" "\N{VOLCANO}\N{CRESCENT MOON}\N{MILKY WAY}" "\N{FOG}\N{THERMOMETER}\N{ROSE}", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"} ) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 200 def test_unicode_invalid_length(app, client, get_message): # From NIST and OWASP - each unicode code point should count as a character. authenticate(client) # Emoji's are 4 bytes in utf-8 data = dict( password="password", new_password="\N{CYCLONE}\N{CYCLONE}\N{FOGGY}\N{FOGGY}", new_password_confirm="\N{CYCLONE}\N{CYCLONE}\N{FOGGY}\N{FOGGY}", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"} ) 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 @pytest.mark.csrf(ignore_unauth=True) @pytest.mark.settings(post_change_view="/post_change_view") def test_csrf(app, client): # enable CSRF, make sure template shows CSRF errors. authenticate(client) data = { "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", } response = client.post("/change", data=data) assert b"The CSRF token is missing" in response.data # Note that we get a CSRF token EVEN for errors - this seems odd # but can't find anything that says its a security issue csrf_token = get_form_input_value(response, "csrf_token") data["csrf_token"] = csrf_token response = client.post("/change", data=data) assert check_location(app, response.location, "/post_change_view") @pytest.mark.csrf(ignore_unauth=True, csrfprotect=True) def test_csrf_json(app, client): # This tests the handle_csrf code path - especially the JSON code path # that should return a JSON response! authenticate(client) data = { "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", } response = client.post("/change", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." # check form path also response = client.post("/change", data=data) assert response.status_code == 400 assert b"The CSRF token is missing." in response.data response = client.get("/change", content_type="application/json") csrf_token = response.json["response"]["csrf_token"] response = client.post("/change", json=data, headers={"X-CSRF-Token": csrf_token}) assert response.status_code == 200 flask-security-5.7.1/tests/test_cli.py000066400000000000000000000312351511046741400200220ustar00rootroot00000000000000""" test_cli ~~~~~~~~ Test command line interface. """ from click.testing import CliRunner import pytest from flask_security.cli import ( roles_add, roles_create, roles_remove, roles_add_permissions, roles_remove_permissions, users_activate, users_change_password, users_create, users_deactivate, users_reset_access, ) from flask_security import verify_password def test_cli_createuser(script_info): """Test create user CLI.""" runner = CliRunner() # Missing params result = runner.invoke(users_create, input="1234\n1234\n", obj=script_info) assert result.exit_code != 0 # Create user with invalid email result = runner.invoke( users_create, ["not-an-email", "--password", "battery staple"], obj=script_info ) assert result.exit_code == 2 # Create user result = runner.invoke( users_create, ["email@example.tld", "--password", "battery staple"], obj=script_info, ) assert result.exit_code == 0 # create user with email and phone number result = runner.invoke( users_create, [ "email1@example.tld", "us_phone_number:5551212", "--password", "battery staple", ], obj=script_info, ) assert result.exit_code == 0 # try to activate using phone number result = runner.invoke(users_activate, "5551212", 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.tld", "security_number:666", "--password", "battery staple", "--active", ], obj=script_info, ) assert result.exit_code == 0 result = runner.invoke(users_activate, ["email1@example.tld"], obj=script_info) assert result.exit_code == 0 assert "was already activated" in result.output app = script_info.load_app() with app.app_context(): user = app.security.datastore.find_user(email="email1@example.tld") assert user.security_number == 666 def test_cli_createuser_unknown_extraargs(script_info): # Test that passing attributes that aren't part of registration form # and aren't part of user model raise an error runner = CliRunner() result = runner.invoke( users_create, [ "email1@example.tld", "mysecretnumber:666", "--password", "battery staple", "--active", ], obj=script_info, ) assert result.exit_code == 2 assert ( "Error: Invalid value: 'mysecretnumber' is an invalid keyword argument" 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.tld", "--password", "battery staple\N{ROMAN NUMERAL ONE}"], obj=script_info, ) assert result.exit_code == 0 assert "email@example.tld" in result.stdout app = script_info.load_app() with app.app_context(): user = app.security.datastore.find_user(email="email@example.tld") assert verify_password( "battery staple\N{LATIN CAPITAL LETTER I}", user.password ) def test_cli_createuser_errors(script_info): # check that errors are stringified runner = CliRunner() result = runner.invoke( users_create, ["--password", "battery staple"], obj=script_info ) assert result.exit_code == 2 assert "Email not provided" in result.output @pytest.mark.babel() @pytest.mark.app_settings(babel_default_locale="fr_FR") def test_cli_locale(script_info): runner = CliRunner() result = runner.invoke( users_create, ["--password", "battery staple"], obj=script_info ) assert result.exit_code == 2 assert "Merci d'indiquer une adresse email" in result.output def test_cli_createrole(script_info): """Test create user CLI.""" runner = CliRunner() # Missing params result = runner.invoke(roles_create, ["-d", "Test description"], obj=script_info) assert result.exit_code != 0 # Create role result = runner.invoke( roles_create, ["superusers", "-d", "Test description"], obj=script_info ) assert result.exit_code == 0 def test_cli_createrole_with_perms(script_info): """Test create user CLI.""" runner = CliRunner() # Create role result = runner.invoke( roles_create, ["superusers", "-d", "Test description", "-p", "super, full-write"], obj=script_info, ) # Some datastores don't support permissions. assert result.exit_code == 0 or result.exit_code == 2 def test_cli_addremove_role(script_info): """Test add/remove role.""" runner = CliRunner() # Create a user and a role result = runner.invoke( users_create, ["a@example.tld", "--password", "battery staple"], obj=script_info ) assert result.exit_code == 0 result = runner.invoke(roles_create, ["superuser"], obj=script_info) assert result.exit_code == 0 # User not found result = runner.invoke( roles_add, ["inval@example.tld", "superuser"], obj=script_info ) assert result.exit_code != 0 # Add: result = runner.invoke(roles_add, ["a@example.tld", "invalid"], obj=script_info) assert result.exit_code != 0 result = runner.invoke( roles_remove, ["inval@example.tld", "superuser"], obj=script_info ) assert result.exit_code != 0 # Remove: result = runner.invoke(roles_remove, ["a@example.tld", "invalid"], obj=script_info) assert result.exit_code != 0 result = runner.invoke( roles_remove, ["b@example.tld", "superuser"], obj=script_info ) assert result.exit_code != 0 result = runner.invoke( roles_remove, ["a@example.tld", "superuser"], obj=script_info ) assert result.exit_code != 0 # Add: result = runner.invoke(roles_add, ["a@example.tld", "superuser"], obj=script_info) assert result.exit_code == 0 result = runner.invoke(roles_add, ["a@example.tld", "superuser"], obj=script_info) assert result.exit_code != 0 # Remove: result = runner.invoke( roles_remove, ["a@example.tld", "superuser"], obj=script_info ) assert result.exit_code == 0 def test_cli_addremove_permissions(script_info): """Test add/remove permissions.""" runner = CliRunner() app = script_info.load_app() 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 ) with app.app_context(): srole = app.security.datastore.find_role("superusers") assert srole.get_permissions() == {"read", "write"} 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"]) with app.app_context(): srole = app.security.datastore.find_role("superusers") assert srole.get_permissions() == set() def test_cli_activate_deactivate(script_info): """Test create user CLI.""" runner = CliRunner() # Create a user result = runner.invoke( users_create, ["a@example.tld", "--password", "battery staple"], obj=script_info ) assert result.exit_code == 0 # Activate result = runner.invoke(users_activate, ["in@valid.org"], obj=script_info) assert result.exit_code != 0 result = runner.invoke(users_deactivate, ["in@valid.org"], obj=script_info) assert result.exit_code != 0 result = runner.invoke(users_activate, ["a@example.tld"], obj=script_info) assert result.exit_code == 0 result = runner.invoke(users_activate, ["a@example.tld"], obj=script_info) assert result.exit_code == 0 # Deactivate result = runner.invoke(users_deactivate, ["a@example.tld"], obj=script_info) assert result.exit_code == 0 result = runner.invoke(users_deactivate, ["a@example.tld"], obj=script_info) assert result.exit_code == 0 def test_cli_reset_user(script_info): runner = CliRunner() result = runner.invoke( users_create, [ "email1@example.tld", "us_phone_number:5551212", "--password", "battery staple", ], obj=script_info, ) result = runner.invoke(users_reset_access, ["5551212"], obj=script_info) assert result.exit_code == 0 result = runner.invoke(users_reset_access, ["5551212"], obj=script_info) assert result.exit_code == 2 result = runner.invoke(users_reset_access, ["4441212"], obj=script_info) assert "User not found" in result.output def test_cli_change_password(script_info): runner = CliRunner() runner.invoke( users_create, ["email1@example.tld", "--password", "battery staple"], obj=script_info, ) result = runner.invoke( users_change_password, ["email1@example.tld", "--password", "battery_staple"], obj=script_info, ) assert result.exit_code == 0 # check too short a password result = runner.invoke( users_change_password, ["email1@example.tld", "--password", "hi"], obj=script_info, ) assert result.exit_code == 2 assert "Password must be at least" in result.output # check that password is properly normalized result = runner.invoke( users_change_password, ["email1@example.tld", "--password", "battery staple\N{ROMAN NUMERAL ONE}"], obj=script_info, ) assert result.exit_code == 0 app = script_info.load_app() with app.app_context(): user = app.security.datastore.find_user(email="email1@example.tld") assert verify_password( "battery staple\N{LATIN CAPITAL LETTER I}", user.password ) result = runner.invoke(users_change_password, ["--help"]) assert "IDENTITY" in result.output # check unknown user result = runner.invoke( users_change_password, ["wrong_email@example.tld", "--password", "battery_staple"], obj=script_info, ) assert result.exit_code == 2 assert "User not found" in result.output @pytest.mark.settings(use_register_v2=False) def test_cli_createuser_old(script_info): """Test create user CLI.""" runner = CliRunner() # Create user result = runner.invoke( users_create, ["email@example.tld", "--password", "battery staple"], obj=script_info, ) assert result.exit_code == 0 @pytest.mark.settings(use_register_v2=False) def test_cli_createuserold_attr(script_info): """Test create user CLI passing attr that is in User but not in form.""" runner = CliRunner() # Create user result = runner.invoke( users_create, [ "email@example.tld", "--password", "battery staple", "us_phone_number:5551212", "--username", "iamuser", ], obj=script_info, ) assert result.exit_code == 0 app = script_info.load_app() with app.app_context(): user = app.security.datastore.find_user(email="email@example.tld") assert user.us_phone_number == "5551212" user = app.security.datastore.find_user(username="iamuser") assert user def test_cli_create_nousername(script_info_min): """Test create user CLI passing attr that is in User but not in form.""" runner = CliRunner() # Create user result = runner.invoke( users_create, [ "email@example.tld", "--password", "battery staple", ], obj=script_info_min, ) assert result.exit_code == 0 flask-security-5.7.1/tests/test_configuration.py000066400000000000000000000025301511046741400221160ustar00rootroot00000000000000""" test_configuration ~~~~~~~~~~~~~~~~~~ Basic configuration tests """ import base64 import pytest from tests.test_utils import authenticate, logout @pytest.mark.settings( logout_url="/custom_logout", login_url="/custom_login", post_login_view="/post_login", post_logout_view="/post_logout", default_http_auth_realm="Custom Realm", ) def test_view_configuration(client, get_message): response = client.get("/custom_login") assert b"

Login

" in response.data response = authenticate(client, endpoint="/custom_login") assert "location" in response.headers assert "/post_login" in response.location response = logout(client, endpoint="/custom_logout") assert "location" in response.headers assert "/post_logout" in response.location response = client.get( "/http", headers={"Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:bogus")}, ) assert response.status_code == 401 assert get_message("UNAUTHENTICATED") 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") def test_template_configuration(client): response = client.get("/login") assert b"CUSTOM LOGIN USER" in response.data flask-security-5.7.1/tests/test_confirmable.py000066400000000000000000000671041511046741400215400ustar00rootroot00000000000000""" test_confirmable ~~~~~~~~~~~~~~~~ Confirmable tests """ from datetime import date, timedelta import re from urllib.parse import parse_qsl, urlsplit import pytest from flask import Flask from freezegun import freeze_time from wtforms.fields import StringField from wtforms.validators import Length from flask_security.core import Security, UserMixin from flask_security.signals import confirm_instructions_sent, user_confirmed from flask_security.forms import SendConfirmationForm from tests.test_utils import ( authenticate, capture_flashes, capture_registrations, check_location, is_authenticated, logout, populate_data, ) pytestmark = pytest.mark.confirmable() @pytest.mark.registerable() def test_confirmable_flag(app, clients, get_message): recorded_confirms = [] recorded_instructions_sent = [] @user_confirmed.connect_via(app) def on_confirmed(app, user): assert isinstance(app, Flask) assert isinstance(user, UserMixin) recorded_confirms.append(user) @confirm_instructions_sent.connect_via(app) def on_instructions_sent(app, **kwargs): assert isinstance(app, Flask) assert isinstance(kwargs["user"], UserMixin) assert isinstance(kwargs["token"], str) assert isinstance(kwargs["confirmation_token"], str) recorded_instructions_sent.append(kwargs["user"]) # Test login before confirmation email = "dude@lp.com" with capture_registrations() as registrations: data = dict( email=email, password="awesome sunset", password_confirm="awesome sunset", next="", ) response = clients.post("/register", data=data) assert response.status_code == 302 response = authenticate(clients, email=email, password="awesome sunset") assert get_message("CONFIRMATION_REQUIRED") in response.data # Test invalid token response = clients.get("/confirm/bogus", follow_redirects=True) assert get_message("INVALID_CONFIRMATION_TOKEN") in response.data # Test JSON response = clients.post( "/confirm", json=dict(email="matt@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/json" assert "user" not in response.json["response"] assert len(recorded_instructions_sent) == 1 # Test ask for instructions with invalid email response = clients.post("/confirm", data=dict(email="bogus@bogus.com")) assert get_message("USER_DOES_NOT_EXIST") in response.data # Test resend instructions 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 = clients.get("/confirm/" + token, follow_redirects=False) assert len(recorded_confirms) == 1 response = clients.get(response.location) assert get_message("EMAIL_CONFIRMED") in response.data # make sure not logged in assert not is_authenticated(clients, get_message) # Test already confirmed response = clients.get("/confirm/" + 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 response = clients.get("/confirm") assert response.status_code == 200 response = clients.post("/confirm", data=dict(email=email)) assert get_message("ALREADY_CONFIRMED") in response.data # Test if user was deleted before confirmation with capture_registrations() as registrations: data = dict( email="mary27@lp.com", password="awesome sunset", password_confirm="awesome sunset", next="", ) clients.post("/register", data=data) user = registrations[0]["user"] token = registrations[0]["confirm_token"] with app.app_context(): app.security.datastore.delete(user) app.security.datastore.commit() if hasattr(app.security.datastore.db, "close_db") and callable( app.security.datastore.db.close_db ): app.security.datastore.db.close_db(None) response = clients.get("/confirm/" + token, follow_redirects=True) assert get_message("INVALID_CONFIRMATION_TOKEN") in response.data @pytest.mark.registerable() def test_confirmation_template(app, client, get_message, outbox): # Check contents of email template - this uses a test template # in order to check all context vars since the default template # doesn't have all of them. recorded_tokens_sent = [] @confirm_instructions_sent.connect_via(app) def on_instructions_sent(app, **kwargs): recorded_tokens_sent.append(kwargs["confirmation_token"]) with capture_registrations() as registrations: data = dict( email="mary@lp.com", password="awesome sunset", password_confirm="awesome sunset", next="", ) # Register - this will use the welcome template client.post("/register", data=data, follow_redirects=True) # Explicitly ask for confirmation - # this will use the confirmation_instructions template client.post("/confirm", data=dict(email="mary@lp.com")) assert len(outbox) == 2 # check registration email matcher = re.findall(r"\w+:.*", outbox[0].body, re.IGNORECASE) # should be 4 - link, email, token, config item assert matcher[1].split(":")[1] == "mary@lp.com" assert matcher[2].split(":")[1] == registrations[0]["confirm_token"] assert matcher[2].split(":")[1] == registrations[0]["confirmation_token"] assert matcher[3].split(":")[1] == "True" # register_blueprint # check confirmation email matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) # should be 4 - link, email, token, config item assert matcher[1].split(":")[1] == "mary@lp.com" assert matcher[2].split(":")[1] == recorded_tokens_sent[0] token = matcher[2].split(":")[1] assert token == recorded_tokens_sent[0] assert matcher[3].split(":")[1] == "True" # register_blueprint # check link _, link = matcher[0].split(":", 1) response = client.get(link, follow_redirects=True) assert get_message("EMAIL_CONFIRMED") 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", password_confirm="awesome sunset" ) response = clients.post("/register", data=data) response = authenticate( clients, email="jyl@lp.com", password="awesome sunset", 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): # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): with capture_registrations() as registrations: data = dict( email="mary@lp.com", password="awesome sunset", password_confirm="awesome sunset", next="", ) client.post("/register", data=data, follow_redirects=True) email = registrations[0]["email"] token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token, follow_redirects=True) msg = get_message("CONFIRMATION_EXPIRED", within="1 milliseconds", email=email) assert msg in response.data @pytest.mark.registerable() def test_email_conflict_for_confirmation_token(app, client, get_message): with capture_registrations() as registrations: data = dict( email="mary@lp.com", password="awesome sunset", password_confirm="awesome sunset", next="", ) client.post("/register", data=data, follow_redirects=True) user = registrations[0]["user"] token = registrations[0]["confirm_token"] # Change the user's email user.email = "tom@lp.com" with app.app_context(): app.security.datastore.put(user) app.security.datastore.commit() response = client.get("/confirm/" + token, follow_redirects=True) msg = get_message("INVALID_CONFIRMATION_TOKEN") assert msg in response.data @pytest.mark.registerable() @pytest.mark.settings(login_without_confirmation=True) def test_login_when_unconfirmed(client, get_message): data = dict( email="mary@lp.com", password="awesome sunset", password_confirm="awesome sunset", next="", ) response = client.post("/register", data=data, follow_redirects=True) assert b"mary@lp.com" in response.data @pytest.mark.registerable() @pytest.mark.settings(password_confirm_required=False) def test_no_auth_token(client_nc): """Make sure that register doesn't return Authentication Token if user isn't confirmed. """ response = client_nc.post( "/register?include_auth_token", json=dict(email="dude@lp.com", password="awesome sunset"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 user = response.json["response"]["user"] 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, password_confirm_required=False) def test_auth_token_unconfirmed(client_nc): """Make sure that register returns Authentication Token if user isn't confirmed, but the 'login_without_confirmation' flag is set. """ response = client_nc.post( "/register?include_auth_token", json=dict(email="dude@lp.com", password="awesome sunset"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 user = response.json["response"]["user"] assert len(user) == 3 and all( k in user for k in ["email", "last_update", "authentication_token"] ) @pytest.mark.registerable() @pytest.mark.settings( login_without_confirmation=True, auto_login_after_confirm=False, password_confirm_required=False, ) def test_confirmation_different_user_when_logged_in_no_auto(client, get_message): """Default - AUTO_LOGIN == false so shouldn't log in second user.""" e1 = "dude@lp.com" e2 = "lady@lp.com" with capture_registrations() as registrations: for e in e1, e2: data = dict(email=e, password="awesome sunset", next="") client.post("/register", data=data) logout(client) token1 = registrations[0]["confirm_token"] token2 = registrations[1]["confirm_token"] client.get("/confirm/" + token1, follow_redirects=True) logout(client) authenticate(client, email=e1) response = client.get("/confirm/" + token2, follow_redirects=True) assert get_message("EMAIL_CONFIRMED") in response.data # should get a login view assert b'Login' in response.data @pytest.mark.registerable() @pytest.mark.settings(login_without_confirmation=True) def test_confirmation_different_user_when_logged_in(client, get_message): # with default no-auto-login - first user should get logged out and second # should be properly confirmed (but not logged in) e1 = "dude@lp.com" e2 = "lady@lp.com" with capture_registrations() as registrations: for e in e1, e2: data = dict( email=e, password="awesome sunset", password_confirm="awesome sunset", next="", ) response = client.post("/register", data=data) assert is_authenticated(client, get_message) logout(client) token1 = registrations[0]["confirm_token"] token2 = registrations[1]["confirm_token"] response = client.get("/confirm/" + token1, follow_redirects=False) assert "/login" in response.location authenticate(client, email=e1, password="awesome sunset") assert is_authenticated(client, get_message) response = client.get("/confirm/" + token2, follow_redirects=True) assert get_message("EMAIL_CONFIRMED") in response.data # first user should have been logged out assert not is_authenticated(client, get_message) authenticate(client, email=e2, password="awesome sunset") assert is_authenticated(client, get_message) @pytest.mark.registerable() @pytest.mark.settings(recoverable=True, password_confirm_required=False) def test_cannot_reset_password_when_email_is_not_confirmed(client, get_message): email = "dude@lp.com" data = dict(email=email, password="awesome sunset", next="") response = client.post("/register", data=data, follow_redirects=True) response = client.post("/reset", data=dict(email=email), follow_redirects=True) assert get_message("CONFIRMATION_REQUIRED") in response.data @pytest.mark.registerable() @pytest.mark.settings(auto_login_after_confirm=False, password_confirm_required=False) def test_confirm_redirect(client, get_message): with capture_registrations() as registrations: data = dict(email="jane@lp.com", password="awesome sunset", next="") client.post("/register", data=data, follow_redirects=True) token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token) assert "location" in response.headers assert "/login" in response.location response = client.get(response.location) assert get_message("EMAIL_CONFIRMED") in response.data @pytest.mark.registerable() @pytest.mark.settings( post_confirm_view="/post_confirm", password_confirm_required=False ) def test_confirm_redirect_to_post_confirm(client, get_message): with capture_registrations() as registrations: data = dict(email="john@lp.com", password="awesome sunset", next="") client.post("/register", data=data, follow_redirects=True) token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token, follow_redirects=False) assert "/post_confirm" in response.location @pytest.mark.registerable() @pytest.mark.settings( redirect_host="localhost:8081", redirect_behavior="spa", post_confirm_view="/confirm-redirect", confirm_error_view="/confirm-error", password_confirm_required=False, ) def test_spa_get(app, client, get_message): """ Test 'single-page-application' style redirects This uses json only. """ with capture_flashes() as flashes: with capture_registrations() as registrations: response = client.post( "/register", json=dict(email="dude@lp.com", password="awesome sunset"), headers={"Content-Type": "application/json"}, ) assert response.headers["Content-Type"] == "application/json" token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/confirm-redirect" == split.path qparams = dict(parse_qsl(split.query)) assert qparams["email"] == "dude@lp.com" response = client.get("/confirm/" + token) split = urlsplit(response.headers["Location"]) qparams = dict(parse_qsl(split.query)) assert response.status_code == 302 assert "/confirm-error" in response.location assert "email" not in qparams assert get_message("ALREADY_CONFIRMED") in qparams["info"].encode("utf-8") # Arguably for json we shouldn't have any - this is buried in register_user # but really shouldn't be. assert len(flashes) == 1 @pytest.mark.registerable() @pytest.mark.settings( confirm_email_within="1 milliseconds", redirect_host="localhost:8081", redirect_behavior="spa", confirm_error_view="/confirm-error", password_confirm_required=False, ) def test_spa_get_bad_token(app, client, get_message): """Test expired and invalid token""" with capture_flashes() as flashes: # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): with capture_registrations() as registrations: response = client.post( "/register", json=dict(email="dude@lp.com", password="awesome sunset"), headers={"Content-Type": "application/json"}, ) assert response.headers["Content-Type"] == "application/json" token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/confirm-error" == split.path qparams = dict(parse_qsl(split.query)) assert "email" not in qparams assert "identity" not in qparams msg = get_message("CONFIRMATION_EXPIRED", within="1 milliseconds") assert msg == qparams["error"].encode("utf-8") # Test mangled token token = ( "WyIxNjQ2MzYiLCIxMzQ1YzBlZmVhM2VhZjYwODgwMDhhZGU2YzU0MzZjMiJd." "BZEw_Q.lQyo3npdPZtcJ_sNHVHP103syjM" "&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d" ) response = client.get("/confirm/" + token) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/confirm-error" == split.path qparams = dict(parse_qsl(split.query)) assert len(qparams) == 1 assert all(k in qparams for k in ["error"]) msg = get_message("INVALID_CONFIRMATION_TOKEN") assert msg == qparams["error"].encode("utf-8") assert len(flashes) == 1 @pytest.mark.filterwarnings("ignore") @pytest.mark.registerable() @pytest.mark.settings(auto_login_after_confirm=True, post_login_view="/postlogin") def test_auto_login(app, client, get_message): with capture_registrations() as registrations: data = dict( email="mary@lp.com", password="password", password_confirm="password", next="", ) client.post("/register", data=data, follow_redirects=True) assert not is_authenticated(client, get_message) token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token, follow_redirects=False) assert check_location(app, response.location, "/postlogin") assert is_authenticated(client, get_message) @pytest.mark.filterwarnings("ignore") @pytest.mark.two_factor() @pytest.mark.registerable() @pytest.mark.settings(two_factor_required=True, auto_login_after_confirm=True) def test_two_factor(app, client, get_message): """If two-factor is enabled, the confirm shouldn't login, but start the 2-factor setup. """ with capture_registrations() as registrations: data = dict( email="mary@lp.com", password="password", password_confirm="password", next="", ) client.post("/register", data=data, follow_redirects=True) assert not is_authenticated(client, get_message) token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token, follow_redirects=False) assert "tf-setup" in response.location @pytest.mark.filterwarnings("ignore") @pytest.mark.two_factor() @pytest.mark.registerable() @pytest.mark.settings(two_factor_required=True, auto_login_after_confirm=True) def test_two_factor_json(app, client, get_message): with capture_registrations() as registrations: data = dict( email="dude@lp.com", password="password", password_confirm="password" ) response = client.post("/register", content_type="application/json", json=data) assert response.headers["content-type"] == "application/json" assert response.json["meta"]["code"] == 200 assert len(response.json["response"]) == 2 assert all(k in response.json["response"] for k in ["csrf_token", "user"]) assert not is_authenticated(client, get_message) token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token, headers={"Accept": "application/json"}) 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}}], username_enable=True, password_confirm_required=False, ) def test_email_not_identity(app, client, get_message): # Test that can register/confirm with email even if it isn't an IDENTITY_ATTRIBUTE 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 not is_authenticated(client, get_message) # 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"][0] # verify that email field not present response = client.get("/login") # this one has a flash containing 'email confirmed' response = client.get("/login") assert b"email" not in response.data assert b"username" in response.data response = client.get("/login", headers={"Content-Type": "application/json"}) assert response.json["response"]["identity_attributes"] == ["username"] # log in with username response = client.post( "/login", data=dict(username="mary", password="awesome sunset"), follow_redirects=True, ) assert b"

Welcome mary

" in response.data @pytest.mark.settings(return_generic_responses=True) def test_generic_response(app, client, get_message): # Confirm matt - then an unknown email - both should get the same answer and # JSON should return 200 recorded_instructions_sent = [] @confirm_instructions_sent.connect_via(app) def on_instructions_sent(app, **kwargs): recorded_instructions_sent.append(kwargs["token"]) response = client.post("/confirm", data=dict(email="matt@lp.com")) assert len(recorded_instructions_sent) == 1 assert get_message("CONFIRMATION_REQUEST", email="matt@lp.com") in response.data response = client.post("/confirm", json=dict(email="matt@lp.com")) assert len(recorded_instructions_sent) == 2 assert response.status_code == 200 # actually confirm matt token = recorded_instructions_sent[0] response = client.get("/confirm/" + token, follow_redirects=True) assert get_message("EMAIL_CONFIRMED") in response.data # Try to confirm an unknown email - should get SAME message as real email. response = client.post("/confirm", data=dict(email="mattwho@lp.com")) assert len(recorded_instructions_sent) == 2 assert get_message("CONFIRMATION_REQUEST", email="mattwho@lp.com") in response.data response = client.post("/confirm", json=dict(email="mattwho@lp.com")) assert len(recorded_instructions_sent) == 2 assert response.status_code == 200 assert not any(e in response.json["response"].keys() for e in ["error", "errors"]) # Try to confirm matt again - should ALSO get same response. response = client.post("/confirm", json=dict(email="matt@lp.com")) assert len(recorded_instructions_sent) == 2 assert response.status_code == 200 def test_generic_with_extra(app, sqlalchemy_datastore): # If application adds a field, make sure we properly return errors # even if 'RETURN_GENERIC_RESPONSES' is set. class MySendConfirmationForm(SendConfirmationForm): recaptcha = StringField("Recaptcha", validators=[Length(min=5)]) app.config["SECURITY_RETURN_GENERIC_RESPONSES"] = True app.config["SECURITY_SEND_CONFIRMATION_TEMPLATE"] = "generic_confirm.html" app.security = Security( app, datastore=sqlalchemy_datastore, send_confirmation_form=MySendConfirmationForm, ) populate_data(app) client = app.test_client() # Test valid user but invalid additional form field # We should get a form error for the extra (invalid) field, no flash bad_data = dict(email="joe@lp.com", recaptcha="1234") good_data = dict(email="joe@lp.com", recaptcha="123456") with capture_flashes() as flashes: response = client.post("/confirm", data=bad_data) assert b"Field must be at least 5" in response.data assert len(flashes) == 0 with capture_flashes() as flashes: response = client.post("/confirm", data=good_data) assert len(flashes) == 1 # JSON with capture_flashes() as flashes: response = client.post("/confirm", json=bad_data) assert response.status_code == 400 assert ( "Field must be at least 5" in response.json["response"]["field_errors"]["recaptcha"][0] ) assert len(flashes) == 0 with capture_flashes() as flashes: response = client.post("/confirm", json=good_data) assert response.status_code == 200 assert len(flashes) == 0 # Try bad email AND bad recaptcha bad_data = dict(email="joe44-lp.com", recaptcha="1234") with capture_flashes() as flashes: response = client.post("/confirm", data=bad_data) assert b"Field must be at least 5" in response.data assert len(flashes) == 0 with capture_flashes() as flashes: response = client.post("/confirm", json=bad_data) assert response.status_code == 400 assert ( "Field must be at least 5" in response.json["response"]["field_errors"]["recaptcha"][0] ) assert len(response.json["response"]["errors"]) == 1 assert len(flashes) == 0 @pytest.mark.flask_async() @pytest.mark.registerable() def test_confirmable_async(app, client, get_message): recorded_confirms = [] recorded_instructions_sent = [] @user_confirmed.connect_via(app) async def on_confirmed(myapp, user): recorded_confirms.append(user) @confirm_instructions_sent.connect_via(app) async def on_instructions_sent(myapp, **kwargs): recorded_instructions_sent.append(kwargs["user"]) email = "dude@lp.com" with capture_registrations() as registrations: data = dict( email=email, password="awesome sunset", password_confirm="awesome sunset", next="", ) response = client.post("/register", data=data) assert response.status_code == 302 client.post( "/confirm", json=dict(email=email), headers={"Content-Type": "application/json"}, ) assert len(recorded_instructions_sent) == 1 # Test confirm token = registrations[0]["confirm_token"] client.get("/confirm/" + token, follow_redirects=False) assert len(recorded_confirms) == 1 flask-security-5.7.1/tests/test_context_processors.py000066400000000000000000000213151511046741400232170ustar00rootroot00000000000000""" test_context_processors ~~~~~~~~~~~~~~~~~~~~~~~ Context processor tests """ import pytest 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() @pytest.mark.registerable() @pytest.mark.confirmable() @pytest.mark.changeable() @pytest.mark.change_email() @pytest.mark.username_recovery() @pytest.mark.change_username() @pytest.mark.settings( login_without_confirmation=True, change_email_template="custom_security/change_email.html", change_password_template="custom_security/change_password.html", login_user_template="custom_security/login_user.html", reset_password_template="custom_security/reset_password.html", forgot_password_template="custom_security/forgot_password.html", send_confirmation_template="custom_security/send_confirmation.html", register_user_template="custom_security/register_user.html", verify_template="custom_security/verify.html", username_recovery_template="custom_security/recover_username.html", change_username_template="custom_security/change_username.html", ) def test_context_processors(client, app, outbox): @app.security.context_processor def default_ctx_processor(): return {"global": "global"} @app.security.forgot_password_context_processor def forgot_password(): return {"foo": "bar-forgot"} response = client.get("/reset") assert b"global" in response.data assert b"bar-forgot" in response.data @app.security.login_context_processor def login(): return {"foo": "bar-login"} response = client.get("/login") assert b"global" in response.data assert b"bar-login" in response.data @app.security.verify_context_processor def verify(): 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-verify" in response.data logout(client) @app.security.register_context_processor def register(): return {"foo": "bar-register"} response = client.get("/register") assert b"global" in response.data assert b"bar-register" in response.data @app.security.reset_password_context_processor def reset_password(): return {"foo": "bar-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-reset" in response.data @app.security.change_password_context_processor def change_password(): return {"foo": "bar-change"} authenticate(client) response = client.get("/change") assert b"global" in response.data assert b"bar-change" in response.data @app.security.send_confirmation_context_processor def send_confirmation(): return {"foo": "bar-confirm"} response = client.get("/confirm") assert b"global" in response.data assert b"bar-confirm" in response.data @app.security.mail_context_processor def mail(): return {"foo": "bar-mail"} client.get("/logout") client.post("/reset", data=dict(email="matt@lp.com")) email = outbox[1] assert "global" in email.body assert "bar-mail" in email.body @app.security.change_email_context_processor def change_email(): return {"foo": "bar-change-email"} authenticate(client) response = client.get("/change-email") assert b"global" in response.data assert b"bar-change-email" in response.data @app.security.recover_username_context_processor def recover_username(): return {"foo": "bar-recover-username"} client.get("/logout") response = client.get("/recover-username") assert b"global" in response.data assert b"bar-recover-username" in response.data @app.security.change_username_context_processor def change_username(): return {"foo": "bar-change-username"} authenticate(client) response = client.get("/change-username") assert b"global" in response.data assert b"bar-change-username" in response.data @pytest.mark.passwordless() @pytest.mark.settings(send_login_template="custom_security/send_login.html") def test_passwordless_login_context_processor(app, client): @app.security.send_login_context_processor def send_login(): return {"foo": "bar-send-login"} response = client.get("/login") 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_setup_template="custom_security/tf_setup.html", two_factor_verify_code_template="custom_security/tf_verify.html", ) def test_two_factor_context_processors(client, app): # Test two factor context processors @app.security.context_processor def default_ctx_processor(): return {"global": "global"} @app.security.tf_setup_context_processor def send_two_factor_setup(): 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-tfsetup" in response.data logout(client) @app.security.tf_token_validation_context_processor def send_two_factor_token_validation(): return {"foo": "bar-tfvalidate"} tf_authenticate(app, client, validate=False) response = client.get("/tf-rescue") assert b"global" in response.data assert b"bar-tfvalidate" in response.data logout(client) @pytest.mark.unified_signin() @pytest.mark.settings( us_setup_template="custom_security/us_setup.html", us_signin_template="custom_security/us_signin.html", us_verify_template="custom_security/us_verify.html", ) def test_unified_signin_context_processors(client, app): @app.security.context_processor def default_ctx_processor(): return {"global": "global"} @app.security.us_signin_context_processor def signin_ctx(): return {"foo": "signin"} # signin template is used in 3 places (TODO test POST us-send-code) response = client.get("/us-signin") assert b"CUSTOM UNIFIED SIGN IN" in response.data assert b"global" in response.data assert b"signin" in response.data response = client.post("/us-signin/send-code") assert b"CUSTOM UNIFIED SIGN IN" in response.data assert b"global" in response.data assert b"signin" in response.data @app.security.us_setup_context_processor def setup_ctx(): return {"foo": "setup"} us_authenticate(client) response = client.get("us-setup") assert b"CUSTOM UNIFIED SIGNIN SETUP" in response.data assert b"global" in response.data assert b"setup" in response.data response = client.post("us-setup", data=dict(chosen_method="sms", phone="555-1212")) assert b"CUSTOM UNIFIED SIGNIN SETUP" in response.data assert b"global" in response.data assert b"setup" in response.data @app.security.us_verify_context_processor def verify_ctx(): return {"foo": "setup"} us_authenticate(client) response = client.get("us-verify") assert b"CUSTOM UNIFIED VERIFY" in response.data assert b"global" in response.data assert b"setup" in response.data response = client.post( "us-verify", data=dict(chosen_method="sms", phone="555-1212") ) assert b"CUSTOM UNIFIED VERIFY" in response.data assert b"global" in response.data assert b"setup" in response.data @pytest.mark.two_factor() @pytest.mark.settings( multi_factor_recovery_template="custom_security/mf_recovery.html", multi_factor_recovery_codes_template="custom_security/mf_recovery_codes.html", multi_factor_recovery_codes=True, ) def test_mf_recovery_context_processors(client, app): @app.security.context_processor def default_ctx_processor(): return {"global": "global"} @app.security.mf_recovery_codes_context_processor def codes_ctx(): return {"foo": "codes"} authenticate(client) response = client.get("/mf-recovery-codes") assert b"global" in response.data assert b"codes" in response.data logout(client) @app.security.mf_recovery_context_processor def code_ctx(): return {"foo": "code"} authenticate(client, "gal@lp.com") response = client.get("/mf-recovery") assert b"global" in response.data assert b"code" in response.data flask-security-5.7.1/tests/test_csrf.py000066400000000000000000000551561511046741400202200ustar00rootroot00000000000000""" test_csrf ~~~~~~~~~~~~~~~~~ CSRF tests :copyright: (c) 2019-2024 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from contextlib import contextmanager from datetime import date, timedelta import flask_wtf.csrf import pytest from flask_wtf import CSRFProtect from freezegun import freeze_time from flask import render_template_string from flask_security import Security, auth_required from tests.test_utils import get_form_input_value, get_session, logout REAL_VALIDATE_CSRF = None @contextmanager def mp_validate_csrf(): """Make sure we are really calling CSRF validation and getting correct answer""" orig_validate_csrf = flask_wtf.csrf.validate_csrf try: mp = MpValidateCsrf(orig_validate_csrf) flask_wtf.csrf.validate_csrf = mp.mp_validate_csrf yield mp finally: flask_wtf.csrf.validate_csrf = orig_validate_csrf class MpValidateCsrf: success = 0 failure = 0 def __init__(self, real_validate_csrf): MpValidateCsrf.success = 0 MpValidateCsrf.failure = 0 global REAL_VALIDATE_CSRF REAL_VALIDATE_CSRF = real_validate_csrf @staticmethod def mp_validate_csrf(data, secret_key=None, time_limit=None, token_key=None): try: REAL_VALIDATE_CSRF(data, secret_key, time_limit, token_key) MpValidateCsrf.success += 1 except Exception: MpValidateCsrf.failure += 1 raise def _get_csrf_token(client): response = client.get( "/login", data={}, headers={"Content-Type": "application/json"} ) return response.json["response"]["csrf_token"] def json_login( client, email="matt@lp.com", password="password", endpoint=None, use_header=False, remember=None, ): # Return tuple (auth_token, csrf_token) csrf_token = _get_csrf_token(client) data = dict(email=email, password=password, remember=remember) if use_header: headers = {"X-CSRF-Token": csrf_token} else: headers = {} data["csrf_token"] = csrf_token response = client.post( endpoint or "/login?include_auth_token", content_type="application/json", json=data, headers=headers, ) assert response.status_code == 200 rd = response.json["response"] return rd["user"]["authentication_token"], rd["csrf_token"] def json_logout(client): response = client.post("logout", content_type="application/json", data={}) assert response.status_code == 200 assert response.json["meta"]["code"] == 200 return response @pytest.mark.csrf() def test_login_csrf(app, client): # This shouldn't log in - but return login form with csrf token. data = dict(email="matt@lp.com", password="password", remember="y") response = client.post("/login", data=data) assert response.status_code == 200 assert b"The CSRF token is missing." in response.data data["csrf_token"] = get_form_input_value(response, "csrf_token") response = client.post("/login", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Welcome matt" in response.data response = logout(client, follow_redirects=True) assert response.status_code == 200 assert b"Log in" in response.data def test_login_csrf_double(app, client): # Test if POST login while already logged in - just redirects to POST_LOGIN app.config["WTF_CSRF_ENABLED"] = True # This shouldn't log in - but return login form with csrf token. data = dict(email="matt@lp.com", password="password", remember="y") response = client.post("/login", data=data) assert response.status_code == 200 assert b"csrf_token" in response.data data["csrf_token"] = _get_csrf_token(client) response = client.post("/login", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Welcome matt" in response.data data["csrf_token"] = _get_csrf_token(client) # Note - should redirect to POST_LOGIN with current user ignoring form data. data["email"] = "newguy@me.com" response = client.post("/login", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Welcome matt" in response.data @pytest.mark.csrf() def test_login_csrf_json(app, client): with mp_validate_csrf() as mp: auth_token, csrf_token = json_login(client) assert auth_token assert csrf_token # Should be just one call to validate - since CSRFProtect not enabled. assert mp.success == 1 and mp.failure == 0 response = json_logout(client) session = get_session(response) assert "csrf_token" not in session @pytest.mark.csrf(csrfprotect=True) def test_login_csrf_json_header(app, client): with mp_validate_csrf() as mp: auth_token, csrf_token = json_login(client, use_header=True) assert auth_token assert csrf_token assert mp.success == 1 and mp.failure == 0 json_logout(client) @pytest.mark.settings(csrf_ignore_unauth_endpoints=True) def test_login_csrf_unauth_ok(app, client): app.config["WTF_CSRF_ENABLED"] = True with mp_validate_csrf() as mp: # This should log in. data = dict(email="matt@lp.com", password="password", remember="y") response = client.post("/login", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Welcome matt" in response.data assert mp.success == 0 and mp.failure == 0 logout(client) @pytest.mark.settings(csrf_ignore_unauth_endpoints=True) def test_login_csrf_unauth_double(app, client, get_message): # Test double login w/o CSRF returns unauth required error message. app.config["WTF_CSRF_ENABLED"] = True # This should log in. data = dict(email="matt@lp.com", password="password", remember="y") response = client.post("/login", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Welcome matt" in response.data # login in again - should work response = client.post("/login", content_type="application/json", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "ANONYMOUS_USER_REQUIRED" ) @pytest.mark.csrf() @pytest.mark.recoverable() def test_reset(app, client): """Test that form-based CSRF works for /reset""" response = client.get("/reset", content_type="application/json") csrf_token = response.json["response"]["csrf_token"] with mp_validate_csrf() as mp: data = dict(email="matt@lp.com") # should fail - no CSRF token - should get a JSON response response = client.post("/reset", content_type="application/json", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." # test template also has error - since using just Flask-WTF form based CSRF - # should be an error on the csrf_token field. response = client.post("/reset", data=data) assert b'class="fs-error-msg">The CSRF token is missing' in response.data # test sending csrf_token works - JSON data["csrf_token"] = csrf_token response = client.post("/reset", content_type="application/json", json=data) assert response.status_code == 200 # test sending csrf_token works - forms response = client.post("/reset", data=data) assert b"Send password reset instructions" in response.data assert mp.success == 2 and mp.failure == 2 @pytest.mark.recoverable() @pytest.mark.csrf(csrfprotect=True) def test_cp_reset(app, client): """Test that header based CSRF works for /reset when using WTF_CSRF_CHECK_DEFAULT=False. """ with mp_validate_csrf() as mp: data = dict(email="matt@lp.com") # should fail - no CSRF token response = client.post("/reset", content_type="application/json", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." csrf_token = _get_csrf_token(client) response = client.post( "/reset", content_type="application/json", json=data, headers={"X-CSRF-Token": csrf_token}, ) assert response.status_code == 200 assert mp.success == 1 and mp.failure == 1 @pytest.mark.changeable() @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(csrf_header="X-XSRF-Token") def test_cp_with_token(app, client): # Make sure can use returned CSRF-Token in Header. # Since the csrf token isn't in the form - must enable app-wide CSRF # using CSRFProtect() - as the above mark does. # Using X-XSRF-Token as header tests that we properly # add that as a known header to WTFforms. auth_token, csrf_token = json_login(client, use_header=True) # make sure returned csrf_token works in header. data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) with mp_validate_csrf() as mp: response = client.post( "/change", content_type="application/json", json=data, headers={"X-XSRF-Token": csrf_token}, ) assert response.status_code == 200 assert mp.success == 1 and mp.failure == 0 json_logout(client) def test_cp_login_json_no_session(app, sqlalchemy_datastore): # Test with global CSRFProtect on and not sending cookie - nothing works. app.config["WTF_CSRF_ENABLED"] = True CSRFProtect(app) app.security = Security(app=app, datastore=sqlalchemy_datastore) client_nc = app.test_client(use_cookies=False) # This shouldn't log in - and will return 400 with mp_validate_csrf() as mp: data = dict(email="matt@lp.com", password="password", remember="y") response = client_nc.post( "/login", content_type="application/json", json=data, headers={"Accept": "application/json"}, ) assert response.status_code == 400 # This still wont work since we don't send a session cookie response = client_nc.post( "/login", content_type="application/json", json=data, headers={"X-CSRF-Token": _get_csrf_token(client_nc)}, ) assert response.status_code == 400 # Although failed - CSRF should have been called assert mp.failure == 2 @pytest.mark.settings(CSRF_PROTECT_MECHANISMS=["basic", "session"]) def test_cp_config(app, sqlalchemy_datastore): # Test improper config (must have WTF_CSRF_CHECK_DEFAULT false if setting # CSRF_PROTECT_MECHANISMS from flask_security import Security app.config["WTF_CSRF_ENABLED"] = True CSRFProtect(app) # The check is done on first request. with pytest.raises(ValueError) as ev: Security(app=app, datastore=sqlalchemy_datastore) assert "must be set to False" in str(ev.value) @pytest.mark.settings(CSRF_PROTECT_MECHANISMS=["basic", "session"]) def test_cp_config2(app, sqlalchemy_datastore): # Test improper config (must have CSRFProtect configured if setting # CSRF_PROTECT_MECHANISMS from flask_security import Security app.config["WTF_CSRF_ENABLED"] = True with pytest.raises(ValueError) as ev: Security(app=app, datastore=sqlalchemy_datastore) assert "CsrfProtect not part of application" in str(ev.value) @pytest.mark.changeable() @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(CSRF_PROTECT_MECHANISMS=["basic", "session"]) def test_different_mechanisms(app, client): # Verify that using token doesn't require CSRF, but sessions do with mp_validate_csrf() as mp: auth_token, csrf_token = json_login(client, use_header=True) # session based change password should fail data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert b"The CSRF token is missing" in response.data # token based should work response = client.post( "/change", json=data, headers={ "Content-Type": "application/json", "Authentication-Token": auth_token, }, ) assert response.status_code == 200 assert mp.success == 1 and mp.failure == 1 @pytest.mark.changeable() @pytest.mark.settings( CSRF_PROTECT_MECHANISMS=["basic", "session"], csrf_ignore_unauth_endpoints=True ) def test_different_mechanisms_nc(app, client_nc): # Verify that using token and no session cookie works # Note that we had to disable unauth_endpoints since you can't log in # w/ CSRF if you don't send in the session cookie. app.config["WTF_CSRF_ENABLED"] = True app.config["WTF_CSRF_CHECK_DEFAULT"] = False CSRFProtect(app) with mp_validate_csrf() as mp: auth_token, csrf_token = json_login(client_nc) # token based should work data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) response = client_nc.post( "/change", json=data, headers={ "Content-Type": "application/json", "Authentication-Token": auth_token, }, ) assert response.status_code == 200 assert mp.success == 0 and mp.failure == 0 @pytest.mark.changeable() @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(csrf_protect_mechanisms=[]) def test_cp_with_token_empty_mechanisms(app, client): # If no mechanisms - shouldn't do any CSRF auth_token, csrf_token = json_login(client, use_header=True) # make sure returned csrf_token works in header. data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) response = client.post( "/change", content_type="application/json", json=data, headers={ "Content-Type": "application/json", "Authentication-Token": auth_token, }, ) assert response.status_code == 200 @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(csrf_ignore_unauth_endpoints=True, CSRF_COOKIE_NAME="XSRF-Token") def test_csrf_cookie(app, client): json_login(client) assert client.get_cookie("XSRF-Token") # Make sure cleared on logout response = client.post("/logout", content_type="application/json") assert response.status_code == 200 assert not client.get_cookie("XSRF-Token") @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(CSRF_COOKIE={"key": "XSRF-Token"}) @pytest.mark.changeable() def test_cp_with_token_cookie(app, client): # Make sure can use returned CSRF-Token cookie in Header when changing password json_login(client, use_header=True) # make sure returned csrf_token works in header. data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) csrf_token = client.get_cookie("XSRF-Token") with mp_validate_csrf() as mp: response = client.post( "/change", content_type="application/json", json=data, headers={"X-XSRF-Token": csrf_token.value}, ) assert response.status_code == 200 assert mp.success == 1 and mp.failure == 0 json_logout(client) assert not client.get_cookie("XSRF-Token") @pytest.mark.csrf(csrfprotect=True) @pytest.mark.app_settings(wtf_csrf_time_limit=1) @pytest.mark.settings(CSRF_COOKIE_NAME="XSRF-Token", csrf_ignore_unauth_endpoints=True) @pytest.mark.changeable() def test_cp_with_token_cookie_expire(app, client): # Make sure that we get a new Csrf-Token cookie if expired. # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): json_login(client, use_header=True) # time unfrozen so should be expired data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) csrf_token = client.get_cookie("XSRF-Token") with mp_validate_csrf() as mp: response = client.post( "/change", content_type="application/json", json=data, headers={"X-XSRF-Token": csrf_token.value}, ) assert response.status_code == 400 assert b"expired" in response.data # Should have gotten a new CSRF cookie value new_csrf_token = client.get_cookie("XSRF-Token") assert csrf_token.value != new_csrf_token.value # 2 failures since the utils:csrf_cookie_handler will check assert mp.success == 0 and mp.failure == 2 json_logout(client) assert not client.get_cookie("XSRF-Token") @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings( CSRF_COOKIE_NAME="XSRF-Token", CSRF_COOKIE_REFRESH_EACH_REQUEST=True ) @pytest.mark.changeable() def test_cp_with_token_cookie_refresh(app, client): # Test CSRF_COOKIE_REFRESH_EACH_REQUEST json_login(client, use_header=True) # make sure returned csrf_token works in header. data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) csrf_cookie = client.get_cookie("XSRF-Token") with mp_validate_csrf() as mp: # Delete cookie - we should always get a new one client.delete_cookie("XSRF-Token") response = client.post( "/change", content_type="application/json", json=data, headers={"X-XSRF-Token": csrf_cookie.value}, ) assert response.status_code == 200 assert client.get_cookie("XSRF-Token") 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("XSRF-Token") response = client.get("/change") assert response.status_code == 200 assert client.get_cookie("XSRF-Token") json_logout(client) assert not client.get_cookie("XSRF-Token") @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(CSRF_COOKIE_NAME="XSRF-Token") @pytest.mark.changeable() def test_remember_login_csrf_cookie(app, client): # Test csrf cookie upon resuming a remember session # Login with remember_token generation json_login(client, use_header=True, remember=True) client.delete_cookie("XSRF-Token") client.delete_cookie("session") # Do a simple get request with the remember_token cookie present assert client.get_cookie("remember_token") response = client.get("/profile") assert response.status_code == 200 assert client.get_cookie("session") assert client.get_cookie("XSRF-Token") # Logout and check that everything cleans up nicely json_logout(client) assert not client.get_cookie("remember_token") assert not client.get_cookie("session") assert not client.get_cookie("XSRF-Token") @pytest.mark.csrf(csrfprotect=True) @pytest.mark.registerable() @pytest.mark.settings(csrf_header="X-CSRF-Token", password_confirm_required=False) def test_json_register_csrf_with_ignore_unauth_set_to_false(app, client): """ Test that you are able to register a user when using the JSON api and the CSRF_IGNORE_UNAUTH_ENDPOINTS is set to False. """ csrf_token = client.get("/login", headers={"Accept": "application/json"}).json[ "response" ]["csrf_token"] email = "eg@testuser.com" data = {"email": email, "password": "password"} response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." response = client.post( "/register", json=data, headers={"Content-Type": "application/json", "X-CSRF-Token": csrf_token}, ) assert response.status_code == 200 assert response.json["response"]["user"]["email"] == email @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings( csrf_protect_mechanisms=["session"], csrf_ignore_unauth_endpoints=True ) def test_myform(app, client): # Create app form - and make sure protect_mechanisms properly skips CSRF # For this test - we don't configure CSRFProtect - just use form CSRF from flask_wtf import FlaskForm from wtforms import StringField class custom_form(FlaskForm): name = StringField("Name") @app.route("/custom", methods=["GET", "POST"]) @auth_required() def custom(): form = custom_form() if form.validate_on_submit(): return render_template_string(f"Nice POST {form.name.data}") return render_template_string( f"Hi {form.name.data}, anything wrong? {form.errors}" ) auth_token, csrf_token = json_login(client, use_header=True) # using session - POST should fail - no CSRF response = client.post("/custom", json={"name": "first POST"}) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." # use CSRF token - should work response = client.post( "/custom", json={"name": "second POST"}, headers={"X-XSRF-Token": csrf_token} ) assert response.status_code == 200 # try with form input response = client.post( "/custom", data={"name": "form POST", "csrf_token": csrf_token} ) assert response.data == b"Nice POST form POST" # now try authenticating via token - shouldn't need CSRF token client_nc = app.test_client(use_cookies=False) response = client_nc.post( "/custom", json={"name": "authtoken POST"}, headers={ "Content-Type": "application/json", "Authentication-Token": auth_token, }, ) assert b"CSRF" not in response.data @pytest.mark.csrf(csrfprotect=True) def test_csrf_json_protect(app, client): # test sending CSRF token in json body for an unauth endpoint (/login) # In older code the @unauth_csrf() decorator would 'fall through' - if the # decorator CSRF checked failed it would fall through to the form CSRF check. # The decorator CSRF check returns a 400 JSON response. csrf_token = _get_csrf_token(client) response = client.post( "/login", json=dict(email="matt@lp.com", password="password", csrf_token=csrf_token), ) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." flask-security-5.7.1/tests/test_datastore.py000066400000000000000000000562211511046741400212430ustar00rootroot00000000000000""" test_datastore ~~~~~~~~~~~~~~ Datastore tests :copyright: (c) 2012 by Matt Wright. :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import pytest from pytest import raises, skip, importorskip from tests.test_utils import init_app_with_options, capture_queries from flask_security import ( RoleMixin, Security, UserMixin, LoginForm, RegisterFormV2, naive_utcnow, ) from flask_security.datastore import Datastore, UserDatastore class User(UserMixin): pass class Role(RoleMixin): pass class MockDatastore(UserDatastore): def put(self, model): pass def delete(self, model): pass def test_unimplemented_datastore_methods(): datastore = Datastore(None) assert datastore.db is None with raises(NotImplementedError): datastore.put(None) with raises(NotImplementedError): datastore.delete(None) assert not datastore.commit() def test_unimplemented_user_datastore_methods(): datastore = UserDatastore(None, None) with raises(NotImplementedError): datastore.find_user() with raises(NotImplementedError): datastore.find_role(None) def test_toggle_active(): datastore = MockDatastore(None, None) user = User() user.active = True assert datastore.toggle_active(user) is True assert not user.active assert datastore.toggle_active(user) is True assert user.active is True def test_deactivate_user(): datastore = MockDatastore(None, None) user = User() user.active = True assert datastore.deactivate_user(user) is True assert not user.active def test_activate_user(): datastore = MockDatastore(None, None) user = User() user.active = False assert datastore.activate_user(user) is True assert user.active is True def test_deactivate_returns_false_if_already_false(): datastore = UserDatastore(None, None) user = User() user.active = False assert not datastore.deactivate_user(user) def test_activate_returns_false_if_already_true(): datastore = UserDatastore(None, None) user = User() user.active = True assert not datastore.activate_user(user) @pytest.mark.parametrize( "ds", ["sqlalchemy_datastore", "fsqlalite_datastore", "sqlalchemy_session_datastore"], ) def test_find_user(request, app, ds): ds = request.getfixturevalue(ds) init_app_with_options(app, ds) with app.app_context(): user_id = ds.find_user(email="gene@lp.com").fs_uniquifier with capture_queries(ds) as queries: assert user_id == ds.find_user(security_number=889900).fs_uniquifier assert len(queries) == 1 assert queries[0].is_select assert user_id == ds.find_user(username="gene").fs_uniquifier @pytest.mark.parametrize( "ds", ["sqlalchemy_datastore", "fsqlalite_datastore", "sqlalchemy_session_datastore"], ) @pytest.mark.settings(join_user_roles=False) def test_find_user_no_joined_load(request, app, ds): ds = request.getfixturevalue(ds) init_app_with_options(app, ds) with app.app_context(): with capture_queries(ds) as queries: user = ds.find_user(security_number=889900) assert len(user.roles) == 1 assert len(queries) == 2 assert queries[0].is_select assert queries[0].statement.column_descriptions[0]["name"] == "User" assert queries[1].statement.column_descriptions[0]["name"] == "Role" def test_find_role(app, datastore): init_app_with_options(app, datastore) with app.app_context(): role = datastore.find_role("admin") assert role is not None role = datastore.find_role("bogus") assert role is None def test_add_role_to_user(app, datastore): init_app_with_options(app, datastore) with app.app_context(): # Test with user object user = datastore.find_user(email="matt@lp.com") assert user.has_role("editor") is False assert datastore.add_role_to_user(user, "editor") is True assert datastore.add_role_to_user(user, "editor") is False 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 @pytest.mark.parametrize( "ds", ["sqlalchemy_datastore", "fsqlalite_datastore", "sqlalchemy_session_datastore"], ) def test_create_user_with_roles(request, app, ds): ds = request.getfixturevalue(ds) init_app_with_options(app, ds) with app.app_context(): role = ds.find_role("admin") user = ds.create_user( email="dude@lp.com", username="dude", password="password", roles=[role] ) ds.commit() with capture_queries(ds) as queries: user = ds.find_user(email="dude@lp.com") assert user.has_role("admin") is True assert len(queries) == 1 assert queries[0].is_select def test_create_user_no_side_effects(app, datastore): init_app_with_options(app, datastore) with app.app_context(): datastore.find_role("admin") roles = ["admin"] datastore.commit() datastore.create_user( email="dude@lp.com", username="dude", password="password", roles=roles ) assert all(isinstance(role, str) for role in roles) def test_delete_user(app, datastore): init_app_with_options(app, datastore) with app.app_context(): user = datastore.find_user(email="matt@lp.com") datastore.delete_user(user) datastore.commit() user = datastore.find_user(email="matt@lp.com") assert user is None def test_access_datastore_from_factory(app, datastore): security = Security() security.init_app(app, datastore) assert security.datastore is not None assert security.app is not None def test_access_datastore_from_app_factory_pattern(app, datastore): security = Security(datastore=datastore) security.init_app(app) assert security.datastore is not None assert security.app is not None def test_init_app_kwargs_override_constructor_kwargs(app, datastore): class ConLoginForm(LoginForm): pass class ConRegisterForm(RegisterFormV2): pass class InitLoginForm(LoginForm): pass security = Security( datastore=datastore, login_form=ConLoginForm, register_form=ConRegisterForm, ) security.init_app(app, login_form=InitLoginForm) assert security.forms["login_form"].cls == InitLoginForm assert security.forms["register_form"].cls == ConRegisterForm def test_create_user_with_roles_and_permissions(app, datastore): ds = datastore if not hasattr(ds.role_model, "permissions"): return init_app_with_options(app, ds) with app.app_context(): role = ds.create_role(name="test1", permissions={"read"}) ds.commit() user = ds.create_user( email="dude@lp.com", username="dude", password="password", roles=[role] ) ds.commit() user = ds.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_types(app, datastore): # Test permissions as a list, set, tuple, comma separated list ds = datastore if not hasattr(ds.role_model, "permissions"): return init_app_with_options(app, ds) with app.app_context(): perms = ["read", "write"] ds.create_role(name="test1", permissions=perms) ds.commit() t1 = ds.find_role("test1") assert {"read", "write"} == t1.get_permissions() perms = {"read", "write"} ds.create_role(name="test2", permissions=perms) ds.commit() t2 = ds.find_role("test2") assert {"read", "write"} == t2.get_permissions() perms = "read, write" ds.create_role(name="test3", permissions=perms) ds.commit() t3 = ds.find_role("test3") assert {"read", "write"} == t3.get_permissions() perms = ("read", "write") ds.create_role(name="test4", permissions=perms) ds.commit() t4 = ds.find_role("test4") assert {"read", "write"} == t4.get_permissions() ds.create_role( name="test5", permissions={"read"}, ) ds.commit() t5 = ds.find_role("test5") assert {"read"} == t5.get_permissions() def test_modify_permissions(app, datastore): ds = datastore if not hasattr(ds.role_model, "permissions"): return init_app_with_options(app, ds) with app.app_context(): perms = {"read", "write"} ds.create_role(name="test1", permissions=perms) ds.commit() t1 = ds.find_role("test1") assert perms == t1.get_permissions() if hasattr(t1, "update_datetime"): orig_update_time = t1.update_datetime assert t1.update_datetime <= naive_utcnow() ds.add_permissions_to_role(t1, "execute") ds.commit() t1 = ds.find_role("test1") assert perms.union({"execute"}) == t1.get_permissions() ds.remove_permissions_from_role(t1, "read") ds.commit() t1 = ds.find_role("test1") assert {"write", "execute"} == t1.get_permissions() if hasattr(t1, "update_datetime"): assert t1.update_datetime > orig_update_time def test_get_permissions(app, datastore): """Verify that role.permissions = None works.""" ds = datastore if not hasattr(ds.role_model, "permissions"): return init_app_with_options(app, ds) with app.app_context(): t1 = ds.find_role("simple") assert set() == t1.get_permissions() def test_modify_permissions_multi(app, datastore): ds = datastore if not hasattr(ds.role_model, "permissions"): return init_app_with_options(app, ds) with app.app_context(): perms = ["read", "write"] ds.create_role(name="test1", permissions=perms) ds.commit() t1 = ds.find_role("test1") assert {"read", "write"} == t1.get_permissions() # send in a list ds.add_permissions_to_role(t1, ["execute", "whatever"]) ds.commit() t1 = ds.find_role("test1") assert {"read", "write", "execute", "whatever"} == t1.get_permissions() ds.remove_permissions_from_role(t1, ["read", "whatever"]) ds.commit() assert {"write", "execute"} == t1.get_permissions() # send in a set perms = {"read", "write"} ds.create_role(name="test2", permissions=perms) ds.commit() # add permissions using comma separate string t2 = ds.find_role("test2") ds.add_permissions_to_role(t2, "execute, whatever") ds.commit() t2 = ds.find_role("test2") assert {"read", "write", "execute", "whatever"} == t2.get_permissions() ds.remove_permissions_from_role(t2, {"read", "whatever"}) ds.commit() assert {"write", "execute"} == t2.get_permissions() ds.remove_permissions_from_role(t2, "write, execute") ds.commit() assert t2.get_permissions() == set() # add permissions using a tuple t2 = ds.find_role("test2") ds.add_permissions_to_role(t2, ("execute2", "whatever2")) ds.commit() assert {"whatever2", "execute2"} == t2.get_permissions() def test_uuid(app, request, tmpdir, realdburl): """Test that UUID extension of postgresql works as a primary id for users""" importorskip("flask_sqlalchemy") import uuid from flask_sqlalchemy import SQLAlchemy from sqlalchemy import Boolean, Column, DateTime, Integer, ForeignKey, String from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship, backref from flask_security import SQLAlchemyUserDatastore from tests.conftest import _setup_realdb, _teardown_realdb # UUID type only supported by postgres - not sqlite. if not realdburl or "postgresql" not in realdburl: skip("This test only works on postgres") db_url, db_info = _setup_realdb(realdburl) app.config["SQLALCHEMY_DATABASE_URI"] = db_url db = SQLAlchemy(app) class RolesUsers(db.Model): __tablename__ = "roles_users" id = Column(Integer(), primary_key=True) user_id = Column("user_id", UUID(as_uuid=True), ForeignKey("user.id")) role_id = Column("role_id", UUID(as_uuid=True), ForeignKey("role.id")) class User(db.Model, UserMixin): __tablename__ = "user" id = Column( 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, nullable=True) password = Column(String(255)) active = Column(Boolean()) created_at = Column(DateTime, default=naive_utcnow()) confirmed_at = Column(DateTime()) roles = relationship( "Role", secondary="roles_users", backref=backref("users", lazy="dynamic") ) class Role(db.Model, RoleMixin): __tablename__ = "role" id = Column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True ) name = Column(String(80), unique=True) description = Column(String(255)) # __hash__ is required to avoid the exception # TypeError: unhashable type: 'Role' when saving a User def __hash__(self): return hash(self.name) with app.app_context(): db.create_all() def tear_down(): with app.app_context(): db.drop_all() db.engine.dispose() _teardown_realdb(db_info) request.addfinalizer(tear_down) ds = SQLAlchemyUserDatastore(db, User, Role) app.security = Security(app, datastore=ds) with app.app_context(): user = ds.find_user(email="matt@lp.com") assert not user def test_webauthn(app, datastore): importorskip("webauthn") if not datastore.webauthn_model: skip(f"No WebAuthn model defined for datastore: {datastore.__class__.__name__}") init_app_with_options(app, datastore) with app.app_context(): user = datastore.find_user(email="matt@lp.com") datastore.create_webauthn( user, name="cred1", credential_id=b"1", public_key=b"key", sign_count=0, transports=None, extensions=None, usage="first", device_type="single_device", backup_state=False, ) datastore.commit() cred = datastore.find_webauthn(b"1") assert cred.name == "cred1" user = datastore.find_user(email="matt@lp.com") assert len(user.webauthn) == 1 assert user.webauthn[0].name == "cred1" datastore.delete_webauthn(user.webauthn[0]) datastore.commit() user = datastore.find_user(email="matt@lp.com") assert len(user.webauthn) == 0 def test_webauthn_cascade(app, datastore): importorskip("webauthn") if not datastore.webauthn_model: skip(f"No WebAuthn model defined for datastore: {datastore.__class__.__name__}") init_app_with_options(app, datastore) with app.app_context(): user = datastore.find_user(email="matt@lp.com") datastore.create_webauthn( user, name="cred1", credential_id=b"1", public_key=b"key", sign_count=0, transports=None, extensions=None, usage="first", device_type="single_device", backup_state=False, ) datastore.create_webauthn( user, name="cred2", credential_id=b"2", public_key=b"key", sign_count=0, transports=None, extensions=None, usage="secondary", device_type="single_device", backup_state=False, ) datastore.commit() user = datastore.find_user(email="matt@lp.com") assert len(user.webauthn) == 2 names = [cred.name for cred in user.webauthn] assert set(names) == {"cred1", "cred2"} assert datastore.find_webauthn(b"1") # delete user datastore.delete_user(user) datastore.commit() user = datastore.find_user(email="matt@lp.com") assert not user cred = datastore.find_webauthn(b"1") assert not cred cred = datastore.find_webauthn(b"2") assert not cred def test_mf_recovery_codes(app, datastore): from tests.conftest import PonyUserDatastore if isinstance(datastore, PonyUserDatastore): skip("Pony not supported") init_app_with_options(app, datastore) with app.test_request_context("/"): user = datastore.find_user(email="matt@lp.com") assert hasattr(user, "mf_recovery_codes") assert not datastore.mf_delete_recovery_code(user, 0) datastore.mf_set_recovery_codes(user, ["r1", "r2", "r3"]) datastore.commit() user = datastore.find_user(email="matt@lp.com") codes = datastore.mf_get_recovery_codes(user) assert codes == ["r1", "r2", "r3"] rv = datastore.mf_delete_recovery_code(user, 1) assert rv datastore.commit() rv = datastore.mf_delete_recovery_code(user, 4) assert not rv user = datastore.find_user(email="matt@lp.com") codes = datastore.mf_get_recovery_codes(user) assert codes == ["r1", "r3"] def test_permissions_fsqla_v2(app): importorskip("sqlalchemy") importorskip("flask_sqlalchemy") # Make sure folks with fsqla_v2 work with new AsList column type from sqlalchemy import insert from flask_sqlalchemy import SQLAlchemy from flask_security.models import fsqla_v2 as fsqla from flask_security import Security from flask_security import 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): pass with app.app_context(): db.create_all() meta_data = db.MetaData() meta_data.reflect(db.engine) role_table = meta_data.tables["role"] # Start by manually creating a role in the 4.1.x style stmt = insert(role_table).values( name="r1", description="r1 v41", permissions="read,write" ) with db.engine.connect() as conn: with conn.begin(): conn.execute(stmt) ds = SQLAlchemyUserDatastore(db, User, Role) app.security = Security(app, datastore=ds) with app.app_context(): # Verify can read something written by 4.x r1 = ds.find_role("r1") assert r1.get_permissions() == {"read", "write"} ds.create_role(name="test5", permissions={"read"}) ds.commit() t5 = ds.find_role("test5") assert {"read"} == t5.get_permissions() with app.app_context(): db.engine.dispose() def test_permissions_41(request, app, realdburl): importorskip("sqlalchemy") importorskip("flask_sqlalchemy") # Check compatibility with 4.1 DB from sqlalchemy import Column, insert from flask_sqlalchemy import SQLAlchemy from flask_security.models import fsqla_v2 as fsqla from flask_security import Security from flask_security import SQLAlchemyUserDatastore from tests.conftest import _setup_realdb, _teardown_realdb if realdburl: db_url, db_info = _setup_realdb(realdburl) app.config["SQLALCHEMY_DATABASE_URI"] = db_url else: app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" def tear_down(): if realdburl: with app.app_context(): db.drop_all() db.engine.dispose() _teardown_realdb(db_info) request.addfinalizer(tear_down) db = SQLAlchemy(app) fsqla.FsModels.set_db_info(db) class Role(db.Model, fsqla.FsRoleMixin): # permissions = Column(UnicodeText, nullable=True) # type: ignore from flask_security import AsaList from sqlalchemy.ext.mutable import MutableList # A comma separated list of strings permissions = Column( MutableList.as_mutable(AsaList()), nullable=True # type: ignore ) class User(db.Model, fsqla.FsUserMixin): pass with app.app_context(): db.create_all() meta_data = db.MetaData() meta_data.reflect(db.engine) role_table = meta_data.tables["role"] # Start by manually creating a role in the 4.1.x style stmt = insert(role_table).values( name="r1", description="r1 v41", permissions="read,write" ) with db.engine.connect() as conn: with conn.begin(): conn.execute(stmt) ds = SQLAlchemyUserDatastore(db, User, Role) app.security = Security(app, datastore=ds) with app.app_context(): r1 = ds.find_role("r1") assert r1.get_permissions() == {"read", "write"} with app.app_context(): db.engine.dispose() def test_fsqlalite_table_name(app): importorskip("flask_sqlalchemy_lite") from flask_sqlalchemy_lite import SQLAlchemy from sqlalchemy.orm import DeclarativeBase from flask_security.models import sqla as sqla from flask_security import FSQLALiteUserDatastore app.config |= { "SQLALCHEMY_ENGINES": { "default": "sqlite:///:memory:", }, } db = SQLAlchemy(app) class Model(DeclarativeBase): pass sqla.FsModels.set_db_info( base_model=Model, user_table_name="myuser", role_table_name="myrole", webauthn_table_name="mywebauthn", ) class Role(Model, sqla.FsRoleMixin): __tablename__ = "myrole" class WebAuthn(Model, sqla.FsWebAuthnMixin): __tablename__ = "mywebauthn" class User(Model, sqla.FsUserMixin): __tablename__ = "myuser" with app.app_context(): Model.metadata.create_all(db.engine) ds = FSQLALiteUserDatastore(db, User, Role, WebAuthn) app.security = Security(app, datastore=ds) with app.app_context(): ds.create_role(name="r1") ds.create_user(email="me@lp.com", roles=["r1"]) ds.commit() user = ds.find_user(email="me@lp.com") assert user with app.app_context(): Model.metadata.drop_all(db.engine) db.engine.dispose() def test_null_fs_uniquifier(app, client): # If a record has a null fs_uniquifier - we shouldn't find it. # The only way this might happen is if an app upgrades from a 3.0 Flask-Security # and doesn't properly update their DB. ds = app.security.datastore with app.test_request_context("/"): user = ds.find_user(email="gal@lp.com") user.fs_uniquifier = "" ds.put(user) ds.commit() user = app.security.login_manager.user_callback("") assert not user flask-security-5.7.1/tests/test_entities.py000066400000000000000000000051071511046741400210760ustar00rootroot00000000000000""" test_entities ~~~~~~~~~~~~~ Entity tests """ import inspect import pytest from sqlalchemy import Column from flask_security import RoleMixin, UserMixin from flask_security.models import fsqla, fsqla_v2 from flask_security.core import AnonymousUser class Role(RoleMixin): def __init__(self, name): self.name = name class User(UserMixin): def __init__(self, roles): self.roles = roles def test_role_mixin_equal(): admin1 = Role("admin") admin2 = Role("admin") assert admin1 == admin2 def test_role_mixin_not_equal(): admin = Role("admin") editor = Role("editor") assert admin != editor def test_user_mixin_has_role_with_string(): admin = Role("admin") editor = Role("editor") user = User([admin, editor]) assert user.has_role("admin") is True assert user.has_role("editor") is True assert user.has_role(admin) is True assert user.has_role(editor) is True def test_anonymous_user_has_no_roles(): user = AnonymousUser() assert not user.has_role("admin") def get_user_attributes(cls): boring = dir(type("dummy", (object,), {})) return [item for item in inspect.getmembers(cls) if item[0] not in boring] @pytest.mark.filterwarnings("ignore::sqlalchemy.exc.SAWarning") def test_fsqla_fields(): # basic test to verify no one modified fsqla after shipping. # Not perfect since not checking relationships etc. v1_user_attrs = { "id", "email", "password", "username", "active", "create_datetime", "update_datetime", "fs_uniquifier", "confirmed_at", "current_login_at", "current_login_ip", "last_login_at", "last_login_ip", "login_count", "tf_phone_number", "tf_primary_method", "tf_totp_secret", } 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 = { 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 = { a[0] for a in get_user_attributes(fsqla.FsRoleMixin) if isinstance(a[1], Column) } assert attrs == v1_role_attrs attrs = { a[0] for a in get_user_attributes(fsqla_v2.FsRoleMixin) if isinstance(a[1], Column) } assert attrs == v1_role_attrs flask-security-5.7.1/tests/test_hashing.py000066400000000000000000000147471511046741400207050ustar00rootroot00000000000000""" test_hashing ~~~~~~~~~~~~ hashing tests :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import timeit import pytest from pytest import raises from tests.test_utils import authenticate, init_app_with_options from passlib.hash import argon2, pbkdf2_sha256, django_pbkdf2_sha256, plaintext from flask_security.core import _get_pwd_context, _get_hashing_context from flask_security.utils import ( hash_password, verify_password, get_hmac, verify_and_update_password, ) def test_verify_password_double_hash(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": {"argon2__rounds": 1}, "SECURITY_PASSWORD_SALT": "salty", "SECURITY_PASSWORD_SINGLE_HASH": False, }, ) with app.app_context(): hashed_pwd = hash_password("pass") assert verify_password("pass", hashed_pwd) assert "t=1" in hashed_pwd # Verify double hash assert verify_password("pass", argon2.hash(get_hmac("pass"))) def test_verify_password_single_hash(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": {"argon2__rounds": 1}, "SECURITY_PASSWORD_SALT": None, "SECURITY_PASSWORD_SINGLE_HASH": True, }, ) with app.app_context(): assert verify_password("pass", hash_password("pass")) def test_verify_password_single_hash_list(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": {"argon2__rounds": 1}, "SECURITY_PASSWORD_SALT": "salty", "SECURITY_PASSWORD_SINGLE_HASH": ["django_pbkdf2_sha256", "plaintext"], "SECURITY_PASSWORD_SCHEMES": [ "argon2", "pbkdf2_sha256", "django_pbkdf2_sha256", "plaintext", ], }, ) with app.app_context(): # double hash assert verify_password("pass", hash_password("pass")) assert verify_password("pass", pbkdf2_sha256.hash(get_hmac("pass"))) # single hash assert verify_password("pass", django_pbkdf2_sha256.hash("pass")) assert verify_password("pass", plaintext.hash("pass")) def test_verify_password_backward_compatibility(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": {"argon2__rounds": 1}, "SECURITY_PASSWORD_SINGLE_HASH": False, "SECURITY_PASSWORD_SCHEMES": ["argon2", "plaintext"], }, ) with app.app_context(): # double hash assert verify_password("pass", hash_password("pass")) # single hash assert verify_password("pass", plaintext.hash("pass")) def test_login_with_bcrypt_enabled(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "bcrypt", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": { "bcrypt__rounds": 4, # minimum so test is faster }, "SECURITY_PASSWORD_SALT": "salty", "SECURITY_PASSWORD_SINGLE_HASH": False, }, ) response = authenticate(app.test_client(), follow_redirects=True) assert b"Home Page" in response.data # Above created all accounts using bcrypt - now change over to argon # and verify that verify_and_update_password works app.config["SECURITY_PASSWORD_HASH"] = "argon2" app.config["SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS"] = dict(argon2__rounds=1) app.config["SECURITY_PASSWORD_SCHEMES"] = ["argon2", "bcrypt"] app.security.pwd_context = _get_pwd_context(app) app.security.hashing_context = _get_hashing_context(app) with app.app_context(): user1 = sqlalchemy_datastore.find_user(email="gal@lp.com") assert app.security.pwd_context.identify(user1.password) == "bcrypt" assert verify_and_update_password("password", user1) # above should have rewritten password to argon1 sqlalchemy_datastore.commit() assert verify_and_update_password("password", user1) user1 = sqlalchemy_datastore.find_user(email="gal@lp.com") assert app.security.pwd_context.identify(user1.password) == "argon2" def test_missing_hash_salt_option(app, sqlalchemy_datastore): with raises(RuntimeError): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_SALT": None, "SECURITY_PASSWORD_SINGLE_HASH": False, }, ) def test_verify_password_argon2_opts(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": { "argon2__rounds": 1, "argon2__salt_size": 16, "argon2__hash_len": 16, }, }, ) with app.app_context(): hashed_pwd = hash_password("pass") assert "t=1" in hashed_pwd assert verify_password("pass", hashed_pwd) @pytest.mark.skip def test_bcrypt_speed(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "bcrypt", "SECURITY_PASSWORD_SALT": "salty", "SECURITY_PASSWORD_SINGLE_HASH": False, }, ) with app.app_context(): print(timeit.timeit(lambda: hash_password("pass"), number=100)) @pytest.mark.skip def test_argon2_speed(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": {"argon2__rounds": 10}, }, ) with app.app_context(): print( "Hash time for {} iterations: {}".format( 100, timeit.timeit(lambda: hash_password("pass"), number=100) ) ) flask-security-5.7.1/tests/test_misc.py000066400000000000000000001520401511046741400202040ustar00rootroot00000000000000""" test_misc ~~~~~~~~~~~ Lots of tests :copyright: (c) 2012 by Matt Wright. :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from datetime import timedelta import hashlib from unittest import mock import re import os.path import time import typing as t import pytest from itsdangerous import BadTimeSignature from wtforms.validators import DataRequired, Length from tests.test_utils import ( authenticate, capture_flashes, capture_reset_password_requests, check_location, check_xlation, get_csrf_token, init_app_with_options, json_authenticate, logout, populate_data, reset_fresh, get_form_input, is_authenticated, ) from tests.test_webauthn import HackWebauthnUtil, reg_2_keys from flask import Flask, abort, request, Response from flask_security import Security from flask_security.forms import ( ChangePasswordForm, ConfirmRegisterForm, EmailField, EmailValidation, ForgotPasswordForm, LoginForm, PasswordField, PasswordlessLoginForm, RegisterForm, RegisterFormV2, RequiredLocalize, ResetPasswordForm, SendConfirmationForm, StringField, email_required, valid_user_email, ) from flask_security import auth_required, roles_required from flask_security.utils import ( base_render_json, encode_string, json_error_response, get_request_attr, hash_data, send_mail, uia_email_mapper, uia_phone_mapper, verify_hash, ) from flask_security.core import _get_serializer if t.TYPE_CHECKING: # pragma: no cover from flask.testing import FlaskClient @pytest.mark.recoverable() def test_my_mail_util(app, sqlalchemy_datastore): from flask_security import MailUtil 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": {"mail_util_cls": MyMailUtil}} ) client = app.test_client() client.post("/reset", data=dict(email="matt@lp.com")) def test_register_blueprint_flag(app, sqlalchemy_datastore): app.security = Security( app, datastore=sqlalchemy_datastore, register_blueprint=False ) client = app.test_client() response = client.get("/login") assert response.status_code == 404 @pytest.mark.registerable() @pytest.mark.recoverable() @pytest.mark.changeable() @pytest.mark.settings( USER_IDENTITY_ATTRIBUTES=[ {"email": {"mapper": uia_email_mapper}}, {"username": {"mapper": lambda x: x}}, ] ) @pytest.mark.filterwarnings( "ignore:.*The RegisterForm is deprecated.*:DeprecationWarning" ) def test_basic_custom_forms(app, sqlalchemy_datastore): class MyLoginForm(LoginForm): username = StringField("My Login Username Field") class MyRegisterForm(RegisterForm): email = EmailField("My Register Email Address Field") class MyForgotPasswordForm(ForgotPasswordForm): email = EmailField( "My Forgot Email Address Field", validators=[email_required, EmailValidation(verify=True), valid_user_email], ) class MyResetPasswordForm(ResetPasswordForm): password = StringField("My Reset Password Field") class MyChangePasswordForm(ChangePasswordForm): password = PasswordField("My Change Password Field") app.security = Security( app, datastore=sqlalchemy_datastore, login_form=MyLoginForm, register_form=MyRegisterForm, forgot_password_form=MyForgotPasswordForm, reset_password_form=MyResetPasswordForm, change_password_form=MyChangePasswordForm, ) populate_data(app) client = app.test_client() response = client.get("/login") assert b"My Login Username Field" in response.data response = client.get("/register") assert b"My Register Email Address Field" in response.data response = client.get("/reset") assert b"My Forgot Email Address Field" in response.data with capture_reset_password_requests() as requests: response = client.post("/reset", data=dict(email="matt@lp.com")) token = requests[0]["token"] response = client.get("/reset/" + token) assert b"My Reset Password Field" in response.data authenticate(client) response = client.get("/change") assert b"My Change Password Field" in response.data @pytest.mark.registerable() @pytest.mark.confirmable() @pytest.mark.settings(use_register_v2=False) @pytest.mark.filterwarnings("ignore:.*The ConfirmRegisterForm.*:DeprecationWarning") def test_confirmable_custom_form(app, sqlalchemy_datastore): class MyRegisterForm(ConfirmRegisterForm): email = EmailField("My Register Email Address Field") class MySendConfirmationForm(SendConfirmationForm): email = EmailField("My Send Confirmation Email Address Field") app.security = Security( app, datastore=sqlalchemy_datastore, send_confirmation_form=MySendConfirmationForm, confirm_register_form=MyRegisterForm, ) client = app.test_client() response = client.get("/register") assert b"My Register Email Address Field" in response.data response = client.get("/confirm") assert b"My Send Confirmation Email Address Field" in response.data def test_passwordless_custom_form(app, sqlalchemy_datastore): app.config["SECURITY_PASSWORDLESS"] = True class MyPasswordlessLoginForm(PasswordlessLoginForm): email = EmailField("My Passwordless Email Address Field") app.security = Security( app, datastore=sqlalchemy_datastore, passwordless_login_form=MyPasswordlessLoginForm, ) client = app.test_client() response = client.get("/login") assert b"My Passwordless Email Address Field" in response.data @pytest.mark.parametrize("logout_methods", (["GET", "POST"], ["GET"], ["POST"])) def test_logout_methods(app, sqlalchemy_datastore, logout_methods): init_app_with_options( app, sqlalchemy_datastore, **{"SECURITY_LOGOUT_METHODS": logout_methods} ) client = app.test_client() authenticate(client) response = client.get("/logout", follow_redirects=True) if "GET" in logout_methods: assert response.status_code == 200 authenticate(client) else: assert response.status_code == 405 # method not allowed response = client.post("/logout", follow_redirects=True) if "POST" in logout_methods: assert response.status_code == 200 else: assert response.status_code == 405 # method not allowed def test_logout_methods_none(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{"SECURITY_LOGOUT_METHODS": None} ) client = app.test_client() authenticate(client) response = client.get("/logout", follow_redirects=True) assert response.status_code == 404 response = client.post("/logout", follow_redirects=True) assert response.status_code == 404 def test_passwordless_and_two_factor_configuration_mismatch(app, sqlalchemy_datastore): with pytest.raises(ValueError): init_app_with_options( app, sqlalchemy_datastore, **{"SECURITY_TWO_FACTOR": True, "SECURITY_TWO_FACTOR_ENABLED_METHODS": []}, ) def test_flash_messages_off(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"SECURITY_FLASH_MESSAGES": False} ) client = app.test_client() response = client.get("/profile") assert get_message("LOGIN") not in response.data def test_invalid_hash_scheme(app, sqlalchemy_datastore, get_message): with pytest.raises(ValueError): init_app_with_options( app, sqlalchemy_datastore, **{"SECURITY_PASSWORD_HASH": "bogus"} ) def test_change_hash_type(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "plaintext", "SECURITY_PASSWORD_SALT": None, "SECURITY_PASSWORD_SCHEMES": ["argon2", "plaintext"], }, ) app.config["SECURITY_PASSWORD_HASH"] = "argon2" app.config["SECURITY_PASSWORD_SALT"] = "salty" app.security = Security( app, datastore=sqlalchemy_datastore, register_blueprint=False ) client = app.test_client() response = client.post( "/login", data=dict(email="matt@lp.com", password="password") ) assert response.status_code == 302 response = client.get("/logout") response = client.post( "/login", data=dict(email="matt@lp.com", password="password") ) assert response.status_code == 302 @pytest.mark.settings(hashing_schemes=["hex_md5"], deprecated_hashing_schemes=[]) @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) assert legacy_hash == new_hash def test_hash_data(in_app_context): data = hash_data(b"hello") assert isinstance(data, str) data = hash_data("hellö") assert isinstance(data, str) def test_verify_hash(in_app_context): 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="öööööööööööööööööööööööööööööööööö", password_hash="bcrypt" ) def test_password_unicode_password_salt(client): response = authenticate(client) assert response.status_code == 302 response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data @pytest.mark.registerable() def test_custom_forms_via_config(app, sqlalchemy_datastore): class MyLoginForm(LoginForm): email = StringField("My Login Email Address Field") class MyRegisterForm(RegisterFormV2): email = StringField("My Register Email Address Field") app.config["SECURITY_LOGIN_FORM"] = MyLoginForm app.config["SECURITY_REGISTER_FORM"] = MyRegisterForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() response = client.get("/login") assert b"My Login Email Address Field" in response.data response = client.get("/register") assert b"My Register Email Address Field" in response.data def test_custom_form_instantiator(app, client, get_message): # Test application form instantiation. # This is using the form factory pattern. # Note in this case - Flask-Security doesn't even know the form class name. from flask_security import FormInfo class FormInstantiator: def __init__(self, myservice): self.myservice = myservice def instantiator(self, form_name, form_cls, *args, **kwargs): if form_name == "login_form": return MyLoginForm(*args, service=self.myservice, **kwargs) raise ValueError("Unknown Form") class MyLoginForm(LoginForm): def __init__(self, *args, service=None, **kwargs): super().__init__(*args, **kwargs) self.myservice = service def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): # pragma: no cover return False assert isinstance(self.email.errors, list) if not self.myservice(self.email.data): self.email.errors.append("Not happening") return False return True def login_checker(email): return True if email == "matt@lp.com" else False fi = FormInstantiator(login_checker) app.security.set_form_info("login_form", FormInfo(fi.instantiator)) response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data logout(client) # Try a normally legit user - but our service denies it response = authenticate(client, email="joe@lp.com") assert b"Not happening" in response.data def test_custom_form_instantiator2(app, client, get_message): # Test application form instantiation. # This is using the form clone pattern. # Note in this case - Flask-Security doesn't even know the form class name. app.config["WTF_CSRF_ENABLED"] = True from flask_security import FormInfo class MyLoginForm(LoginForm): def __init__(self, *args, service=None, **kwargs): super().__init__(*args, **kwargs) self.myservice = service def instantiator(self, form_name, form_cls, *args, **kwargs): return MyLoginForm(*args, service=self.myservice, **kwargs) def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): # pragma: no cover return False assert isinstance(self.email.errors, list) if not self.myservice(self.email.data): self.email.errors.append("Not happening") return False return True def login_checker(email): return True if email == "matt@lp.com" else False with app.test_request_context(): fi = MyLoginForm(formdata=None, service=login_checker) app.security.set_form_info("login_form", FormInfo(fi.instantiator)) csrf_token = get_csrf_token(client) response = client.post( "/login", data=dict(email="matt@lp.com", password="password", csrf_token=csrf_token), follow_redirects=True, ) assert b"Welcome matt@lp.com" in response.data logout(client) # Try a normally legit user - but our service denies it csrf_token = get_csrf_token(client) response = client.post( "/login", data=dict(email="joe@lp.com", password="password", csrf_token=csrf_token), ) assert b"Not happening" in response.data def test_custom_form_setting(app, sqlalchemy_datastore): from flask_security import FormInfo security = Security(app=app, datastore=sqlalchemy_datastore) with pytest.raises(ValueError) as vex: security.set_form_info("mylogin_form", FormInfo()) assert "Unknown form name mylogin_form" == str(vex.value) with pytest.raises(ValueError) as vex: security.set_form_info("login_form", FormInfo()) assert "form class must be provided" in str(vex.value) def test_form_required(app, sqlalchemy_datastore): class MyLoginForm(LoginForm): myfield = StringField("My Custom Field", validators=[RequiredLocalize()]) app.config["SECURITY_LOGIN_FORM"] = MyLoginForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() response = client.post("/login", content_type="application/json") assert response.status_code == 400 assert b"myfield" in response.data def test_form_required_local_message(app, sqlalchemy_datastore): """Test having a local message (not xlatable and not part of MSG_ config.""" msg = "hi! did you forget me?" class MyLoginForm(LoginForm): myfield = StringField( "My Custom Field", validators=[RequiredLocalize(message=msg)] ) app.config["SECURITY_LOGIN_FORM"] = MyLoginForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() response = client.post("/login", content_type="application/json") assert response.status_code == 400 assert b"myfield" in response.data assert msg.encode("utf-8") in response.data # WTforms 2.x incorrectly catches ValueError and sets that as the form error. # Our config_value routine raises ValueError for missing config items.. assert b"Key" not in response.data def test_without_babel(app, client): # Test if babel modules exist but we don't init babel - things still work app.config["BABEL_DEFAULT_LOCALE"] = "fr_FR" response = client.get("/login") assert response.status_code == 200 def test_no_email_sender(app, sqlalchemy_datastore, outbox): """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: def __init__(self, email): self.email = email security = Security() security.init_app(app, sqlalchemy_datastore) with app.app_context(): user = TestUser("matt@lp.com") send_mail("Test Default Sender", user.email, "welcome", user=user) assert 1 == len(outbox) assert "test@testme.com" == outbox[0].sender def test_sender_tuple(app, sqlalchemy_datastore, outbox): """Verify that if sender is a (name, address) tuple, in the received email sender is properly formatted as "name
" Flask-Mail takes tuples - Flask-Mailman takes them - however the local-mem backend doesn't format them correctly (SMTP backend doesn't work either?) """ app.config["MAIL_DEFAULT_SENDER"] = ("Test User", "test@testme.com") class TestUser: def __init__(self, email): self.email = email security = Security() security.init_app(app, sqlalchemy_datastore) with app.app_context(): user = TestUser("matt@lp.com") send_mail("Test Tuple Sender", user.email, "welcome", user=user) assert 1 == len(outbox) assert outbox[0].sender == "Test User " def test_send_mail_context(app, sqlalchemy_datastore, outbox): """Test full context sent to MailUtil/send_mail""" app.config["MAIL_DEFAULT_SENDER"] = "test@testme.com" app.security = Security() app.security.init_app(app, sqlalchemy_datastore) class TestUser: def __init__(self, email): self.email = email @app.security.mail_context_processor def mail(): return {"foo": "bar-mail"} with app.app_context(): user = TestUser("matt@lp.com") send_mail("Test Default Sender", user.email, "welcome", user=user) assert 1 == len(outbox) assert "test@testme.com" == outbox[0].sender matcher = re.match( r".*ExtraContext:(\S+).*", outbox[0].body, re.IGNORECASE | re.DOTALL ) assert matcher.group(1) == "bar-mail" @pytest.mark.babel() @pytest.mark.app_settings(babel_default_locale="fr_FR") def test_xlation(app, client): assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" response = client.get("/login") assert b'' in response.data response = authenticate(client) assert response.status_code == 302 response = authenticate(client, follow_redirects=True) assert b"Bienvenue matt@lp.com" in response.data @pytest.mark.babel() @pytest.mark.app_settings(babel_default_locale="fr_FR") def test_myxlation(app, sqlalchemy_datastore, pytestconfig): # Test changing a single MSG and having an additional translation dir # Flask-BabelEx doesn't support lists of directories.. pytest.importorskip("flask_babel") i18n_dirname = [ "builtin", os.path.join(pytestconfig.rootdir, "tests/translations"), ] init_app_with_options( app, sqlalchemy_datastore, **{"SECURITY_I18N_DIRNAME": i18n_dirname} ) assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" app.config["SECURITY_MSG_INVALID_PASSWORD"] = ("Password no-worky", "error") client = app.test_client() response = client.post("/login", data=dict(email="matt@lp.com", password="forgot")) assert b"Passe - no-worky" in response.data @pytest.mark.babel() @pytest.mark.app_settings(babel_default_locale="fr_FR") def test_myxlation_complete(app, sqlalchemy_datastore, pytestconfig): # Test having own translations and not using builtin. pytest.importorskip("flask_babel") i18n_dirname = [ os.path.join(pytestconfig.rootdir, "tests/translations"), ] init_app_with_options( app, sqlalchemy_datastore, **{"SECURITY_I18N_DIRNAME": i18n_dirname} ) assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" app.config["SECURITY_MSG_INVALID_PASSWORD"] = ("Password no-worky", "error") client = app.test_client() response = client.post("/login", data=dict(email="matt@lp.com", password="forgot")) assert b"Passe - no-worky" in response.data @pytest.mark.babel() @pytest.mark.app_settings(babel_default_locale="fr_FR") def test_form_labels(app, sqlalchemy_datastore): app.security = Security() app.security.init_app(app, sqlalchemy_datastore) assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" with app.test_request_context(): rform = RegisterForm() assert str(rform.password.label.text) == "Mot de passe" assert str(rform.password_confirm.label.text) == "Confirmer le mot de passe" assert str(rform.email.label.text) == "Adresse email" assert str(rform.submit.label.text) == "Inscription" form = LoginForm() assert str(form.password.label.text) == "Mot de passe" assert str(form.remember.label.text) == "Se souvenir de moi" assert str(form.email.label.text) == "Adresse email" assert str(form.submit.label.text) == "Connexion" form = ChangePasswordForm() assert str(form.password.label.text) == "Mot de passe" assert str(form.new_password.label.text) == "Nouveau mot de passe" assert str(form.new_password_confirm.label.text) == "Confirmer le mot de passe" assert str(form.submit.label.text) == "Changer le mot de passe" @pytest.mark.babel() @pytest.mark.app_settings(babel_default_locale="fr_FR") def test_wtform_xlation(app, sqlalchemy_datastore): # Make sure wtform xlations work class MyLoginForm(LoginForm): fixed_length = StringField( "FixedLength", validators=[DataRequired(), Length(3, 3)] ) app.security = Security() app.security.init_app(app, datastore=sqlalchemy_datastore, login_form=MyLoginForm) assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" client = app.test_client() response = client.get("/login") assert b'' in response.data data = dict( email="matt@lp.com", password="", remember="y", fixed_length="waytoolong" ) response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 flerror = response.json["response"]["field_errors"]["fixed_length"][0] # This is completely dependent on WTforms translations.... assert ( flerror == "Le doit contenir exactement 3 caractères." or flerror == "Le champ doit contenir exactement 3 caractères." ) @pytest.mark.changeable() @pytest.mark.babel() def test_per_request_xlate(app, client): from flask import request, session babel = app.extensions["babel"] def get_locale(): # For a given session - set lang based on first request. # Honor explicit url request first if "lang" not in session: locale = request.args.get("lang", None) if not locale: locale = request.accept_languages.best if locale: session["lang"] = locale return session.get("lang", None).replace("-", "_") babel.locale_selector_func = get_locale babel.locale_selector = get_locale # Flask-Babel >= 3.0.0 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")]) assert response.status_code == 200 # verify errors are xlated assert b"Merci d'indiquer un mot de passe" in response.data # log in correctly - this should set locale in session data = dict(email="matt@lp.com", password="password", remember="y") response = client.post( "/login", data=data, headers=[("Accept-Language", "fr")], follow_redirects=True ) assert response.status_code == 200 # make sure further requests always get correct xlation w/o sending header response = client.get("/change", follow_redirects=True) assert response.status_code == 200 assert b"Nouveau mot de passe" in response.data assert b"

Changer le mot de passe

" in response.data # try JSON response = client.post( "/change", json=dict(email="matt@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 assert response.json["response"]["field_errors"]["new_password"] == [ "Merci d'indiquer un mot de passe" ] """ This cant work yet due to zxcvbn usage of gettext def test_zxcvbn_xlate(app): class TestUser(object): def __init__(self, email): self.email = email app.config["BABEL_DEFAULT_LOCALE"] = "fr_FR" app.security = Security() app.security.init_app(app) assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" with app.test_request_context(): user = TestUser("jwag@notme.com") pbad, pnorm = app.security.password_util.validate("simple", False, user=user) print(pbad) """ @pytest.mark.settings(password_check_breached="strict") def test_breached(app, sqlalchemy_datastore): # partial response from: https://api.pwnedpasswords.com/range/07003 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" app.security = Security() app.security.init_app(app, sqlalchemy_datastore) with app.test_request_context(): with mock.patch("urllib.request.urlopen") as mock_urlopen: mock_urlopen.return_value.__enter__.return_value.read.return_value = ( pwned_response ) pbad, pnorm = app.security.password_util.validate("flaskflask", False) assert len(pbad) == 1 assert app.config["SECURITY_MSG_PASSWORD_BREACHED"][0] in pbad[0] @pytest.mark.settings( password_check_breached="strict", password_breached_count=16, password_complexity_checker="zxcvbn", ) def test_breached_cnt(app, sqlalchemy_datastore): # partial response from: https://api.pwnedpasswords.com/range/07003 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" app.security = Security() app.security.init_app(app, sqlalchemy_datastore) with app.test_request_context(): with mock.patch("urllib.request.urlopen") as mock_urlopen: mock_urlopen.return_value.__enter__.return_value.read.return_value = ( pwned_response ) 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] @pytest.mark.skip @pytest.mark.settings(password_check_breached="strict") def test_breached_real(app, sqlalchemy_datastore): """Actually go out to internet..""" app.security = Security() app.security.init_app(app, sqlalchemy_datastore) with app.test_request_context(): pbad, pnorm = app.security.password_util.validate("flaskflask", True) assert len(pbad) == 1 assert app.config["SECURITY_MSG_PASSWORD_BREACHED"][0] in pbad[0] def test_json_error_response_string(): """Unit test for correct response when a string is given.""" error_msg = "This is an error!" response = json_error_response(errors=error_msg) assert "field_errors" not in response assert response["errors"][0] == error_msg def test_json_error_response_dict(): """Unit test for correct response when a dict is given.""" error_msg = { "e-mail": ["The e-mail address is already in the system."], "name": ["The name is too long.", "Nice name"], } all_msgs = [] [all_msgs.extend(m) for m in error_msg.values()] response = json_error_response(field_errors=error_msg) assert "errors" in response assert "field_errors" in response assert all(m in response["errors"] for m in all_msgs) def test_json_error_response_typeerror(): """Unit test for checking for error raising.""" error_msg = ("tuple",) with pytest.raises(TypeError): json_error_response(errors=error_msg) def test_json_form_errors(app, client): """Test wtforms form level errors are correctly sent via json""" with app.test_request_context(): form = ChangePasswordForm() form.validate() form.form_errors.append("I am an error") response = base_render_json(form) error_list = response.json["response"]["errors"] assert len(error_list) == 3 assert "I am an error" in error_list def test_method_view(app, client): # auth_required with flask method view from flask.views import MethodView from flask import render_template_string class MyView(MethodView): decorators = [auth_required("token", "session")] def get(self): return render_template_string("Hi view") myview = MyView.as_view("myview") app.add_url_rule("/myview", view_func=myview, methods=["GET"]) response = client.get("/myview", follow_redirects=False) # should require login assert response.status_code == 302 assert "/login" in response.location authenticate(client) response = client.get("/myview") assert response.status_code == 200 assert b"Hi view" in response.data def test_phone_util_override(app, sqlalchemy_datastore): from flask_security import phone_util class MyPhoneUtil(phone_util.PhoneUtil): def validate_phone_number(self, input_data): return "call-me" def get_canonical_form(self, input_data): return "very-canonical" app.security = Security(phone_util_cls=MyPhoneUtil) app.security.init_app(app, sqlalchemy_datastore) with app.app_context(): assert uia_phone_mapper("55") == "very-canonical" def test_authn_freshness( app: "Flask", client: "FlaskClient", get_message: t.Callable[..., bytes] ) -> None: """Test freshness using default reauthn_handler""" @auth_required(within=30, grace=0) def myview(): return Response(status=200) @auth_required(within=0.001, grace=0) def myspecialview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) app.add_url_rule("/myspecialview", view_func=myspecialview, methods=["GET"]) authenticate(client) # This should work and not be redirected response = client.get("/myview", follow_redirects=False) assert response.status_code == 200 # This should require additional authn and redirect to verify time.sleep(0.1) with capture_flashes() as flashes: response = client.get("/myspecialview", follow_redirects=False) assert response.status_code == 302 assert response.location == "/verify?next=/myspecialview" assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "REAUTHENTICATION_REQUIRED" ) # Test json error response response = client.get("/myspecialview", headers={"accept": "application/json"}) assert response.status_code == 401 assert response.json and response.json["response"]["errors"][0].encode( "utf-8" ) == get_message("REAUTHENTICATION_REQUIRED") def test_authn_freshness_handler(app, client, get_message): """Test with our own handler""" @app.security.reauthn_handler def my_reauthn(within, grace, headers=None): assert within == timedelta(minutes=30) or timedelta(minutes=0.001) if app.security._want_json(request): payload = json_error_response(errors="Oh No") return app.security._render_json(payload, 401, headers, None) abort(500) @auth_required(within=30, grace=0) def myview(): return Response(status=200) @auth_required(within=0.001, grace=0) def myspecialview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) app.add_url_rule("/myspecialview", view_func=myspecialview, methods=["GET"]) authenticate(client) # This should work and not be redirected response = client.get("/myview", follow_redirects=False) assert response.status_code == 200 # This should require additional authn time.sleep(0.1) response = client.get("/myspecialview", follow_redirects=False) assert response.status_code == 500 # Test json error response response = client.get("/myspecialview", headers={"accept": "application/json"}) assert response.status_code == 401 assert response.json["response"]["errors"][0] == "Oh No" def test_authn_freshness_callable(app, client, get_message): @auth_required(within=lambda: timedelta(minutes=30)) def myview(): 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 @pytest.mark.settings(url_prefix="/myprefix") def test_default_authn_bp(app, client): """Test default reauthn handler with blueprint prefix""" @auth_required(within=1, grace=0) def myview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) authenticate(client, endpoint="/myprefix/login") # This should require additional authn and redirect to verify reset_fresh(client, within=timedelta(minutes=1)) response = client.get("/myview", follow_redirects=False) assert response.status_code == 302 assert response.location == "/myprefix/verify?next=/myview" def test_authn_freshness_grace(app, client, get_message): # Test that grace override within. @auth_required(within=lambda: timedelta(minutes=30), grace=10) def myview(): return Response(status=200) @auth_required(within=0.001, grace=lambda: timedelta(minutes=10)) def myspecialview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) app.add_url_rule("/myspecialview", view_func=myspecialview, methods=["GET"]) authenticate(client) # This should work and not be redirected response = client.get("/myview", follow_redirects=False) assert response.status_code == 200 # This should NOT require additional authn time.sleep(0.1) response = client.get("/myspecialview", follow_redirects=False) assert response.status_code == 200 def test_authn_freshness_nc(app, client_nc, get_message): # By default, auth token carries the fs_paa time. @auth_required(within=30) def myview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] h = {"Authentication-Token": token} # This should fail - should be a redirect response = client_nc.get("/myview", headers=h, follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(freshness_allow_auth_token=False) def test_authn_freshness_nc_no(app, client_nc, get_message): # If don't send session cookie - then freshness always fails @auth_required(within=30) def myview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] h = {"Authentication-Token": token} # This should fail - should be a redirect response = client_nc.get("/myview", headers=h, follow_redirects=False) assert response.status_code == 302 assert response.location == "/verify?next=/myview" @pytest.mark.two_factor() @pytest.mark.settings(freshness=timedelta(minutes=-1), multi_factor_recovery_codes=True) def test_authn_freshness_nc_no_fresh(app, client_nc, get_message): # test disabling freshness when no session is sent. response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] h = {"Authentication-Token": token} # This should work response = client_nc.get("/tf-setup", json={}, headers=h) assert response.status_code == 200 assert not response.json["response"]["tf_required"] response = client_nc.get("/mf-recovery-codes", json={}, headers=h) assert response.status_code == 200 assert response.json["response"]["recovery_codes"] == [] def test_verify_fresh(app, client, get_message): # Hit a fresh-required endpoint and walk through verify authenticate(client) reset_fresh(client, app.config["SECURITY_FRESHNESS"]) with capture_flashes() as flashes: response = client.get("/fresh", follow_redirects=True) assert b"Reauthenticate" in response.data assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "REAUTHENTICATION_REQUIRED" ) form_response = response.data.decode("utf-8") matcher = re.match( r'.*action="([^"]*)".*', form_response, re.IGNORECASE | re.DOTALL ) verify_url = matcher.group(1) reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get(verify_url) assert b"Reauthenticate" in response.data response = client.post( verify_url, data=dict(password="not my password"), follow_redirects=False ) assert b"Reauthenticate" in response.data response = client.post( verify_url, data=dict(password="password"), follow_redirects=False ) assert check_location(app, response.location, "/fresh") # should be fine now response = client.get("/fresh", follow_redirects=True) assert b"Fresh Only" in response.data def test_verify_fresh_json(app, client, get_message): # Hit a fresh-required endpoint and walk through verify authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get("/fresh", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] response = client.get("/verify") assert b"Reauthenticate" in response.data response = client.post( "/verify", json=dict(password="not my password"), headers=headers ) assert response.status_code == 400 response = client.post("/verify", json=dict(password="password"), headers=headers) assert response.status_code == 200 # should be fine now 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" @pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) def test_verify_wan(app, client, get_message): # test get correct options when requiring a reauthentication and have wan keys # setup. headers = {"Accept": "application/json", "Content-Type": "application/json"} reg_2_keys(client) reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get("/fresh", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] assert response.json["response"]["has_webauthn_verify_credential"] # the verify form should have the webauthn verify form attached response = client.get("verify") assert b'action="/wan-verify"' in response.data app.config["SECURITY_WAN_ALLOW_AS_VERIFY"] = None response = client.get("/fresh", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] assert not response.json["response"]["has_webauthn_verify_credential"] # the verify form should NOT have the webauthn verify form attached response = client.get("verify") assert b'action="/wan-verify"' not in response.data 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 "/root" in response.location response = client.get("/logout") assert response.status_code == 302 assert "/root" in response.location 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 "/post_login" in response.location response = client.get("/logout") assert response.status_code == 302 assert "/post_logout" in response.location def test_open_redirect(app, client, get_message): """ Test various possible URLs that urlsplit() shows as relative but many browsers will interpret as absolute - and thus have a open-redirect vulnerability. """ test_urls = [ ("\\\\\\github.com", "%5C%5C%5Cgithub.com"), (" //github.com", "%20//github.com"), (r"/\github.com", "/%5Cgithub.com"), (r"\/github.com", "%5C/github.com"), ("//github.com", ""), ("\t//github.com", "%09//github.com"), ] for i, o in test_urls: data = dict(email="matt@lp.com", password="password", next=i) response = client.post("/login", data=data, follow_redirects=False) if response.status_code == 302: # this means it passed form validation but should have been quoted assert check_location(app, response.location, o) elif response.status_code == 200: # should have failed form validation assert get_message("INVALID_REDIRECT") in response.data else: raise AssertionError("Bad response code") logout(client) def test_kwargs(): import warnings warnings.simplefilter("error") with pytest.raises(DeprecationWarning): Security(myownkwarg="hi") def test_nodatastore(app): with pytest.raises(ValueError): s = Security(app) s.init_app(app) @pytest.mark.filterwarnings("ignore:.*Replacing login_manager.*:DeprecationWarning") def test_reuse_security_object(sqlalchemy_datastore): # See: https://github.com/pallets-eco/flask-security/issues/518 # Let folks re-use the Security object (mostly for testing). security = Security(datastore=sqlalchemy_datastore) app = Flask(__name__) app.response_class = Response app.debug = True app.config["SECRET_KEY"] = "secret" app.config["TESTING"] = True security.init_app(app) assert hasattr(app, "login_manager") app = Flask(__name__) app.response_class = Response app.debug = True app.config["SECRET_KEY"] = "secret" app.config["TESTING"] = True security.init_app(app) assert hasattr(app, "login_manager") @pytest.mark.settings(static_folder_url="/mystatic/fs") def test_static_url(app, sqlalchemy_datastore): from flask_security import url_for_security from flask import url_for init_app_with_options(app, sqlalchemy_datastore) with app.test_request_context("http://localhost:5001/login"): static_url = url_for_security("static", filename="js/webauthn.js") assert static_url == "/mystatic/fs/js/webauthn.js" static_url = url_for(".static", filename="js/webauthn.js") assert static_url == "/mystatic/fs/js/webauthn.js" def test_multi_app(app, sqlalchemy_datastore): # test that 2 different app with 2 different FS # with USERNAME_ENABLE which dynamically changes the class definition app = Flask(__name__) app.response_class = Response app.debug = True app.config["SECRET_KEY"] = "secret" app.config["TESTING"] = True app.config["SECURITY_USERNAME_ENABLE"] = True security = Security(datastore=sqlalchemy_datastore) security.init_app(app) assert hasattr(security.forms["register_form"].cls, "username") assert "username" in security.user_identity_attributes[1].keys() app = Flask(__name__) app.response_class = Response app.debug = True app.config["SECRET_KEY"] = "secret" app.config["TESTING"] = True app.config["SECURITY_USERNAME_ENABLE"] = True security2 = Security(datastore=sqlalchemy_datastore) security2.init_app(app) assert hasattr(security2.forms["register_form"].cls, "username") assert "username" in security2.user_identity_attributes[1].keys() @pytest.mark.registerable() def test_login_email_whatever(app, client, get_message): # login, by default, shouldn't verify email address is deliverable.. # register etc can/should do that. app.config["SECURITY_EMAIL_VALIDATOR_ARGS"] = {"check_deliverability": True} # register should fail since non-deliverable TLD data = dict( email="dude@me.mytld", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_EMAIL_ADDRESS" ) # login should work since we are just checking for identity response = client.post( "/login", data=dict(email="matt@lp.com", password="password") ) assert response.status_code == 302 assert "/" in response.location @pytest.mark.skip def test_sqlalchemy_session_conn(request, app, tmpdir, realdburl): # test harness for checking connection mgmt by logging all connections from .conftest import sqlalchemy_session_setup ds = sqlalchemy_session_setup( request, app, tmpdir, realdburl, echo_pool="debug", echo="debug" ) init_app_with_options(app, ds) client = app.test_client() client.post("/login", data=dict(email="matt@lp.com", password="password")) client.post("/login", json=dict(noemail="matt@lp.com", password="password")) time.sleep(5) def test_login_required(app, client, get_message): # Test that @login_required calls our default_unauthn_handler from flask_login import login_required @app.route("/loginreq") @login_required def login_req(): pass response = client.get("/loginreq", follow_redirects=False) assert response.location == "/login?next=/loginreq" response = client.get("/loginreq", follow_redirects=True) assert get_message("UNAUTHENTICATED") in response.data def test_simplify_url(): from flask_security.utils import simplify_url s = simplify_url("https://localhost/profile", "https://localhost/login") assert s == "/login" s = simplify_url("https:/myhost/profile", "https://localhost/login") assert s == "https://localhost/login" @pytest.mark.parametrize( "verify_secret_key, verify_fallbacks, should_pass", [ ("new_secret", [], False), # Should fail - only new key ("new_secret", ["old_secret"], True), # Should pass - has fallback ("old_secret", [], True), # Should pass - using original key ("wrong_secret", ["also_wrong"], False), # Should fail - no valid keys ], ids=["new-key-only", "with-fallback", "original-key", "wrong-keys"], ) def test_secret_key_fallbacks(app, verify_secret_key, verify_fallbacks, should_pass): # Create token with original key app.config["SECRET_KEY"] = "old_secret" serializer = _get_serializer(app, "CONFIRM") token = serializer.dumps({"data": "test"}) # Attempt verification with different key configurations app.config["SECRET_KEY"] = verify_secret_key app.config["SECRET_KEY_FALLBACKS"] = verify_fallbacks serializer = _get_serializer(app, "CONFIRM") if should_pass: data = serializer.loads(token) assert data["data"] == "test" else: with pytest.raises(BadTimeSignature): serializer.loads(token) @pytest.mark.settings(username_enable=True) def test_custom_login_form(app, sqlalchemy_datastore, get_message): # Test custom login form that deletes email and uses username only # Also test that if app leave 'email' in as a user identity attribute we # will ignore it class MyLoginForm(LoginForm): email = None app.security = Security( app, datastore=sqlalchemy_datastore, login_form=MyLoginForm, ) populate_data(app) client = app.test_client() response = client.get("/login", follow_redirects=False) assert not get_form_input(response, "email") response = client.post( "/login", json=dict(email="jill@lp.com", password="password") ) assert response.status_code == 400 assert ( get_message("USER_DOES_NOT_EXIST") == response.json["response"]["field_errors"][""][0].encode() ) response = client.post("/login", json=dict(username="jill", password="password")) assert response.status_code == 200 @pytest.mark.settings(password_required=False) def test_password_required_setting(app, sqlalchemy_datastore): with pytest.raises(ValueError) as vex: Security(app=app, datastore=sqlalchemy_datastore) assert "SECURITY_PASSWORD_REQUIRED can only be" in str(vex.value) def test_null_user_id(app, client, get_message): # if DB not configured correctly - make sure we catch null fs_uniquifier json_authenticate(client) assert is_authenticated(client, get_message) with client.session_transaction() as sess: sess["_user_id"] = "" sess["user_id"] = "" assert not is_authenticated(client, get_message) flask-security-5.7.1/tests/test_oauthglue.py000066400000000000000000000310041511046741400212420ustar00rootroot00000000000000""" test_oauthglue.py ~~~~~~~~~~~~~~~~~ Oauth glue tests - oauthglue is a very thin shim between FS and authlib :copyright: (c) 2022-2024 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import pytest import urllib.parse from urllib.parse import parse_qsl, urlsplit from flask import redirect from flask_wtf import CSRFProtect from flask_security import FsOAuthProvider from tests.test_utils import ( authenticate, check_location, get_csrf_token, get_form_action, get_form_input_value, get_session, init_app_with_options, is_authenticated, logout, setup_tf_sms, ) pytestmark = pytest.mark.oauth() class MockRequestsResponse: # authlib returns a Requests Response def __init__(self, contents): self.contents = contents def json(self): return self.contents class MockProvider: def __init__(self, name): self.name = name self.raise_exception = None self.identity = "matt@lp.com" def set_exception(self, raise_exception): self.raise_exception = raise_exception def set_identity(self, email): self.identity = email def get(self, field, token): resp = MockRequestsResponse({"email": self.identity}) return resp def authorize_access_token(self): if self.raise_exception: raise self.raise_exception return "token" def authorize_redirect(self, uri): redirect_url = f"/whatever?redirect_uri={uri}" return redirect(urllib.parse.quote(redirect_url)) class MockOAuth: def __init__(self): pass def register(self, name, **kwargs): setattr(self, name, MockProvider(name)) @pytest.mark.settings(oauth_enable=True, post_login_view="/post_login") @pytest.mark.app_settings(wtf_csrf_enabled=True) def test_github(app, sqlalchemy_datastore, get_message): CSRFProtect(app) init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockOAuth()}} ) client = app.test_client() response = client.get("/login") github_url = get_form_action(response, 1) csrf_token = get_form_input_value(response, field_id="github_csrf_token") # make sure required CSRF response = client.post(github_url, follow_redirects=False) assert b"The CSRF token is missing" in response.data response = client.post( github_url, data=dict(csrf_token=csrf_token), follow_redirects=False ) assert "/whatever" in response.location response = client.get("/login/oauthresponse/github", follow_redirects=False) assert response.status_code == 302 assert "/post_login" in response.location # verify logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings( oauth_enable=True, post_login_view="/post_login", csrf_ignore_unauth_endpoints=True ) @pytest.mark.app_settings(wtf_csrf_enabled=True, wtf_csrf_check_default=False) def test_github_nocsrf(app, sqlalchemy_datastore, get_message): # Test if ignore_unauth_endpoints is true - doesn't require CSRF CSRFProtect(app) init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockOAuth()}} ) client = app.test_client() response = client.get("/login") github_url = get_form_action(response, 1) response = client.post(github_url, follow_redirects=False) assert "/whatever" in response.location @pytest.mark.settings(oauth_enable=True, post_login_view="/post_login") def test_outside_register(app, sqlalchemy_datastore, get_message): def myoauth_fetch_identity(oauth, token): resp = oauth.myoauth.get("user", token=token) profile = resp.json() return "email", profile["email"] authlib_oauth = MockOAuth() authlib_oauth.register("myoauth") init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": authlib_oauth}} ) # Have to register with Oauthglue. app.security.oauthglue.register_provider("myoauth", None, myoauth_fetch_identity) client = app.test_client() response = client.get("/login") myoauth_url = get_form_action(response, 2) response = client.post(myoauth_url, follow_redirects=False) assert "/whatever" in response.location response = client.get("/login/oauthresponse/myoauth", follow_redirects=False) assert response.status_code == 302 assert "/post_login" in response.location # verify logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(oauth_enable=True) def test_bad_api(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockOAuth()}} ) client = app.test_client() response = client.post("/login/oauthstart/foobar") assert response.status_code == 404 response = client.get("/login/oauthresponse/foobar") assert response.status_code == 404 from authlib.integrations.base_client.errors import MismatchingStateError oauth_app = app.security.oauthglue.oauth_app oauth_app.github.set_exception(MismatchingStateError) response = client.get("/login/oauthresponse/github", follow_redirects=True) assert response.status_code == 200 assert ( get_message( "OAUTH_HANDSHAKE_ERROR", exerror="mismatching_state", exdesc="CSRF Warning! State not equal in request and response.", ) in response.data ) @pytest.mark.settings(oauth_enable=True) def test_unknown_user(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockOAuth()}} ) client = app.test_client() oauth_app = app.security.oauthglue.oauth_app oauth_app.github.set_identity("jwag@lp.com") response = client.get("/login/oauthresponse/github", follow_redirects=True) assert get_message("IDENTITY_NOT_REGISTERED", id="jwag@lp.com") in response.data @pytest.mark.two_factor() @pytest.mark.settings(oauth_enable=True) def test_tf(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockOAuth()}} ) client = app.test_client() authenticate(client) sms_sender = setup_tf_sms(client) logout(client) response = client.get("/login?next=/profile") github_url = get_form_action(response, 1) response = client.post(github_url, follow_redirects=False) assert "/whatever" in response.location redirect_url = urllib.parse.urlsplit(urllib.parse.unquote(response.location)) local_redirect = urllib.parse.parse_qs(redirect_url.query)["redirect_uri"][0] response = client.get(local_redirect, follow_redirects=True) sendcode_url = get_form_action(response, 0) response = client.post( sendcode_url, data=dict(code=sms_sender.messages[0].split()[-1]), follow_redirects=True, ) assert b"Profile Page" in response.data @pytest.mark.settings( oauth_enable=True, redirect_host="myui.com:8090", redirect_behavior="spa", login_error_view="/login-error", post_oauth_login_view="/post-login", csrf_ignore_unauth_endpoints=False, ) @pytest.mark.app_settings(wtf_csrf_enabled=True) def test_spa(app, sqlalchemy_datastore, get_message): CSRFProtect(app) headers = {"Accept": "application/json", "Content-Type": "application/json"} init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockOAuth()}} ) client = app.test_client() csrf_token = get_csrf_token(client) headers["X-CSRF-Token"] = csrf_token response = client.post("/login/oauthstart/github", headers=headers) assert "/whatever" in response.location redirect_url = urllib.parse.urlsplit(urllib.parse.unquote(response.location)) local_redirect = urllib.parse.parse_qs(redirect_url.query)["redirect_uri"][0] response = client.get(local_redirect, headers=headers) assert response.status_code == 302 split = urlsplit(response.location) assert "myui.com:8090" == split.netloc assert "/post-login" == split.path qparams = dict(parse_qsl(split.query)) assert qparams["email"] == "matt@lp.com" # try unknown user - should redirect to login_error_view oauth_app = app.security.oauthglue.oauth_app oauth_app.github.set_identity("jwag@lp.com") response = client.get("/login/oauthresponse/github", follow_redirects=False) split = urlsplit(response.location) assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) assert ( qparams["error"] == get_message("IDENTITY_NOT_REGISTERED", id="jwag@lp.com").decode() ) # try fake oauth exception from authlib.integrations.base_client.errors import MismatchingStateError oauth_app.github.set_exception(MismatchingStateError) response = client.get("/login/oauthresponse/github", follow_redirects=False) split = urlsplit(response.location) assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) msg = get_message( "OAUTH_HANDSHAKE_ERROR", exerror="mismatching_state", exdesc="CSRF Warning! State not equal in request and response.", ) assert qparams["error"] == msg.decode() @pytest.mark.settings(oauth_enable=True, post_login_view="/post-login") def test_already_auth(app, sqlalchemy_datastore, get_message): headers = {"Accept": "application/json", "Content-Type": "application/json"} init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockOAuth()}} ) client = app.test_client() authenticate(client) assert is_authenticated(client, get_message) # json response = client.post("/login/oauthstart/github", headers=headers) assert response.status_code == 400 # forms response = client.post("/login/oauthstart/github", follow_redirects=False) assert response.status_code == 302 check_location(app, response.location, "/post-login") @pytest.mark.settings(oauth_enable=True, post_login_view="/post-login") def test_simple_next(app, sqlalchemy_datastore, get_message): # For oauth we stash 'next' in the session since we can't really # send it all around the oauth providers. init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockOAuth()}} ) client = app.test_client() response = client.get("/profile", follow_redirects=True) github_url = get_form_action(response, 1) response = client.post(github_url, follow_redirects=False) assert "/whatever" in response.location session = get_session(response) assert "fs_oauth_next" in session response = client.get("/login/oauthresponse/github", follow_redirects=False) assert response.status_code == 302 assert check_location(app, response.location, "/profile") session = get_session(response) assert "fs_oauth_next" not in session @pytest.mark.settings(oauth_enable=True, post_login_view="/post_login") def test_provider_class(app, sqlalchemy_datastore, get_message): from authlib.integrations.base_client.errors import MismatchingStateError class MyOauthProvider(FsOAuthProvider): def fetch_identity_cb(self, oauth, token): resp = oauth.myoauth.get("user", token=token) profile = resp.json() return "email", profile["email"] def oauth_response_failure(self, e): return redirect("/uh-oh") init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockOAuth()}} ) # Have to register with Oauthglue. app.security.oauthglue.register_provider_ext(MyOauthProvider("myoauth")) client = app.test_client() response = client.get("/login") myoauth_url = get_form_action(response, 2) response = client.post(myoauth_url, follow_redirects=False) assert "/whatever" in response.location # test error - and that our handler is called oauth_app = app.security.oauthglue.oauth_app oauth_app.myoauth.set_exception(MismatchingStateError) response = client.get("/login/oauthresponse/myoauth", follow_redirects=False) assert response.status_code == 302 assert check_location(app, response.location, "/uh-oh") # now log in successfully oauth_app.myoauth.set_exception(None) response = client.get("/login/oauthresponse/myoauth", follow_redirects=False) assert response.status_code == 302 assert check_location(app, response.location, "/post_login") assert is_authenticated(client, get_message) flask-security-5.7.1/tests/test_passwordless.py000066400000000000000000000171411511046741400220040ustar00rootroot00000000000000""" test_passwordless ~~~~~~~~~~~~~~~~~ Passwordless tests """ from datetime import date, timedelta import re from urllib.parse import parse_qsl, urlsplit import warnings import pytest from flask import Flask from freezegun import freeze_time from tests.test_utils import ( capture_flashes, capture_passwordless_login_requests, logout, ) from flask_security import Security, UserMixin, login_instructions_sent pytestmark = pytest.mark.passwordless() def test_passwordless_flag(app, client, get_message, outbox): recorded = [] @login_instructions_sent.connect_via(app) def on_instructions_sent(app, user, login_token): assert isinstance(app, Flask) assert isinstance(user, UserMixin) assert isinstance(login_token, str) recorded.append(user) # Test disabled account response = client.post( "/login", data=dict(email="tiya@lp.com"), follow_redirects=True ) assert get_message("DISABLED_ACCOUNT") in response.data # Test login with json and valid email data = dict(email="matt@lp.com") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 200 assert len(recorded) == 1 assert len(outbox) == 1 # Test login with json and invalid email data = dict(email="nobody@lp.com") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) assert b"errors" in response.data # Test sends email and shows appropriate response with capture_passwordless_login_requests() as requests: response = client.post( "/login", data=dict(email="matt@lp.com"), follow_redirects=True ) assert len(recorded) == 2 assert len(requests) == 1 assert len(outbox) == 2 assert "user" in requests[0] assert "login_token" in requests[0] user = requests[0]["user"] assert get_message("LOGIN_EMAIL_SENT", email=user.email) in response.data token = requests[0]["login_token"] response = client.get("/login/" + token, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") in response.data # Test already authenticated response = client.get("/login/" + token, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") not in response.data logout(client) # Test invalid token response = client.get("/login/bogus", follow_redirects=True) assert get_message("INVALID_LOGIN_TOKEN") in response.data # Test login request with invalid email response = client.post("/login", data=dict(email="bogus@bogus.com")) assert get_message("USER_DOES_NOT_EXIST") in response.data def test_passwordless_template(app, client, get_message, outbox): # Check contents of email template - this uses a test template # in order to check all context vars since the default template # doesn't have all of them. with capture_passwordless_login_requests() as requests: client.post("/login", data=dict(email="joe@lp.com"), follow_redirects=True) assert len(outbox) == 1 matcher = re.findall(r"\w+:.*", outbox[0].body, re.IGNORECASE) # should be 4 - link, email, token, config item assert matcher[1].split(":")[1] == "joe@lp.com" assert matcher[2].split(":")[1] == requests[0]["login_token"] assert matcher[3].split(":")[1] == "True" # register_blueprint # check link link = matcher[0].split(":", 1)[1] response = client.get(link, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") in response.data @pytest.mark.settings(login_within="1 milliseconds") def test_expired_login_token(client, app, get_message): e = "matt@lp.com" with capture_passwordless_login_requests() as requests: # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): client.post("/login", data=dict(email=e), follow_redirects=True) token = requests[0]["login_token"] user = requests[0]["user"] response = client.get("/login/" + token, follow_redirects=True) assert ( get_message("LOGIN_EXPIRED", within="1 milliseconds", email=user.email) in response.data ) @pytest.mark.settings( redirect_host="localhost:8081", redirect_behavior="spa", post_login_view="/login-redirect", ) def test_spa_get(app, client): """ Test 'single-page-application' style redirects This uses json only. """ with capture_flashes() as flashes: with capture_passwordless_login_requests() as requests: response = client.post( "/login", json=dict(email="matt@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.headers["Content-Type"] == "application/json" token = requests[0]["login_token"] response = client.get("/login/" + token) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/login-redirect" == split.path qparams = dict(parse_qsl(split.query)) assert qparams["email"] == "matt@lp.com" assert len(flashes) == 0 @pytest.mark.settings( login_within="1 milliseconds", redirect_host="localhost:8081", redirect_behavior="spa", login_error_view="/login-error", ) def test_spa_get_bad_token(app, client, get_message): """Test expired and invalid token""" with capture_flashes() as flashes: with capture_passwordless_login_requests() as requests: # Note that we need relatively new-ish date since session cookies expire. with freeze_time(date.today() + timedelta(days=-1)): response = client.post( "/login", json=dict(email="matt@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.headers["Content-Type"] == "application/json" token = requests[0]["login_token"] response = client.get("/login/" + token) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) 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") # Test mangled token token = ( "WyIxNjQ2MzYiLCIxMzQ1YzBlZmVhM2VhZjYwODgwMDhhZGU2YzU0MzZjMiJd." "BZEw_Q.lQyo3npdPZtcJ_sNHVHP103syjM" "&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d" ) response = client.get("/login/" + token) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) assert len(qparams) == 1 assert all(k in qparams for k in ["error"]) msg = get_message("INVALID_LOGIN_TOKEN") assert msg == qparams["error"].encode("utf-8") assert len(flashes) == 0 def test_deprecated(app, sqlalchemy_datastore): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") security = Security() security.init_app(app, sqlalchemy_datastore) assert any("passwordless feature" in str(m.message) for m in w) flask-security-5.7.1/tests/test_phone_util.py000066400000000000000000000012241511046741400214140ustar00rootroot00000000000000""" test_phone_util ~~~~~~~~~~~~~~~ Tests for PhoneUtil class in flask_security.phone_util. Covers phone number validation and canonicalization logic. """ import pytest from flask_security.phone_util import PhoneUtil # Use default app fixture from conftest.py # Override config using the recommended settings marker @pytest.mark.settings(phone_region_default="US") @pytest.mark.parametrize( "input_number", ["123456", "bad-number-%%%", "+999", "abcdefgh"] ) def test_invalid_phone_numbers_return_none(app, input_number): with app.app_context(): phone_util = PhoneUtil(app) assert phone_util.get_canonical_form(input_number) is None flask-security-5.7.1/tests/test_recoverable.py000066400000000000000000001061011511046741400215370ustar00rootroot00000000000000""" test_recoverable ~~~~~~~~~~~~~~~~ Recoverable functionality tests :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from datetime import date, timedelta import re from urllib.parse import parse_qsl, urlsplit import pytest from flask import Flask from freezegun import freeze_time from wtforms.fields import StringField from wtforms.validators import Length from tests.test_utils import ( authenticate, capture_flashes, capture_reset_password_requests, check_location, get_form_input_value, is_authenticated, logout, populate_data, ) from flask_security.core import Security, UserMixin from flask_security.forms import ForgotPasswordForm, LoginForm from flask_security.signals import ( password_reset, reset_password_instructions_sent, username_recovery_email_sent, ) pytestmark = pytest.mark.recoverable() def test_recoverable_flag(app, clients, get_message, outbox): recorded_resets = [] recorded_instructions_sent = [] @password_reset.connect_via(app) def on_password_reset(app, user): recorded_resets.append(user) @reset_password_instructions_sent.connect_via(app) def on_instructions_sent(app, **kwargs): assert isinstance(app, Flask) assert isinstance(kwargs["user"], UserMixin) assert isinstance(kwargs["token"], str) recorded_instructions_sent.append(kwargs["user"]) # Test the reset view 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: response = clients.post( "/reset", data=dict(email="joe@lp.com"), follow_redirects=True ) assert len(recorded_instructions_sent) == 1 assert len(outbox) == 1 assert response.status_code == 200 assert get_message("PASSWORD_RESET_REQUEST", email="joe@lp.com") in response.data token = requests[0]["token"] # Test view for reset token response = clients.get("/reset/" + token) assert b"

Reset password

" in response.data # Test submitting a new password but leave out confirm 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 = clients.post( "/reset/" + token, data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, ) assert get_message("PASSWORD_RESET_NO_LOGIN") in response.data assert len(recorded_resets) == 1 logout(clients) # Test logging in with the new password response = authenticate( clients, "joe@lp.com", "awesome sunset", follow_redirects=True ) assert b"Welcome joe@lp.com" in response.data logout(clients) # Test invalid email response = clients.post( "/reset", data=dict(email="bogus@lp.com"), follow_redirects=True ) assert get_message("USER_DOES_NOT_EXIST") in response.data logout(clients) # Test invalid token response = clients.post( "/reset/bogus", data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, ) assert get_message("INVALID_RESET_PASSWORD_TOKEN") in response.data # Test mangled token token = ( "WyIxNjQ2MzYiLCIxMzQ1YzBlZmVhM2VhZjYwODgwMDhhZGU2YzU0MzZjMiJd." "BZEw_Q.lQyo3npdPZtcJ_sNHVHP103syjM" "&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d" ) response = clients.post( "/reset/" + token, data={"password": "newpassword", "password_confirm": "newpassword"}, follow_redirects=True, ) 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", password_confirm="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, outbox): recorded_resets = [] recorded_instructions_sent = [] @password_reset.connect_via(app) def on_password_reset(app, user): recorded_resets.append(user) @reset_password_instructions_sent.connect_via(app) def on_instructions_sent(app, **kwargs): recorded_instructions_sent.append(kwargs["user"]) with capture_flashes() as flashes: # Test reset password creates a token and sends email with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="joe@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.headers["Content-Type"] == "application/json" assert len(recorded_instructions_sent) == 1 assert len(outbox) == 1 assert response.status_code == 200 token = requests[0]["token"] # Test invalid email response = client.post( "/reset", json=dict(email="whoknows@lp.com"), ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "USER_DOES_NOT_EXIST" ) # Test submitting a new password but leave out 'confirm' response = client.post( "/reset/" + token, json=dict(password="newpassword"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "PASSWORD_NOT_PROVIDED" ) # Test submitting a new password response = client.post( "/reset/" + token + "?include_auth_token", json=dict(password="awesome sunset", password_confirm="awesome sunset"), ) assert not response.json["response"] assert len(recorded_resets) == 1 # reset automatically logs user in logout(client) # Test logging in with the new password response = client.post( "/login?include_auth_token", json=dict(email="joe@lp.com", password="awesome sunset"), headers={"Content-Type": "application/json"}, ) assert all( k in response.json["response"]["user"] for k in ["email", "authentication_token"] ) logout(client) # Use token again - should fail since already have set new password. response = client.post( "/reset/" + token, json=dict(password="newpassword", password_confirm="newpassword"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 assert len(recorded_resets) == 1 # Test invalid token response = client.post( "/reset/bogus", json=dict(password="newpassword", password_confirm="newpassword"), headers={"Content-Type": "application/json"}, ) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_RESET_PASSWORD_TOKEN" ) assert len(flashes) == 0 def test_recoverable_template(app, client, get_message, outbox): # Check contents of email template - this uses a test template # in order to check all context vars since the default template # doesn't have all of them. with capture_reset_password_requests() as resets: response = client.post( "/reset", data=dict(email="joe@lp.com"), follow_redirects=True ) assert len(outbox) == 1 matcher = re.findall(r"\w+:.*", outbox[0].body, re.IGNORECASE) # should be 4 - link, email, token, config item assert matcher[1].split(":")[1] == "joe@lp.com" assert matcher[2].split(":")[1] == resets[0]["reset_token"] assert matcher[3].split(":")[1] == "True" # register_blueprint assert matcher[4].split(":")[1] == "/reset" # SECURITY_RESET_URL # check link link = matcher[0].split(":", 1)[1] response = client.get(link, follow_redirects=True) assert b"Reset Password" in response.data 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"), ) 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"), ) assert response.status_code == 200 # try to access protected endpoint with old session - shouldn't work response = other_client.get("/profile") assert response.status_code == 302 assert response.location == "/login?next=/profile" def test_login_form_description(app, sqlalchemy_datastore): app.security = Security(app, datastore=sqlalchemy_datastore) with app.test_request_context("/login"): login_form = LoginForm() expected = 'Forgot password?' assert login_form.password.description == expected @pytest.mark.settings(reset_password_within="1 milliseconds") def test_expired_reset_token(client, get_message): # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="joe@lp.com"), follow_redirects=True) user = requests[0]["user"] token = requests[0]["token"] with capture_flashes() as flashes: msg = get_message( "PASSWORD_RESET_EXPIRED", within="1 milliseconds", email=user.email ) # Test getting reset form with expired token response = client.get("/reset/" + token, follow_redirects=True) assert msg in response.data # Test trying to reset password with expired token response = client.post( "/reset/" + token, data={"password": "newpassword", "password_confirm": "newpassword"}, follow_redirects=True, ) assert msg in response.data assert len(flashes) == 2 def test_bad_reset_token(client, get_message): # Test invalid token - get form response = client.get("/reset/bogus", follow_redirects=True) assert get_message("INVALID_RESET_PASSWORD_TOKEN") in response.data # Test invalid token - reset password response = client.post( "/reset/bogus", data={"password": "newpassword", "password_confirm": "newpassword"}, follow_redirects=True, ) assert get_message("INVALID_RESET_PASSWORD_TOKEN") in response.data # Test mangled token token = ( "WyIxNjQ2MzYiLCIxMzQ1YzBlZmVhM2VhZjYwODgwMDhhZGU2YzU0MzZjMiJd." "BZEw_Q.lQyo3npdPZtcJ_sNHVHP103syjM" "&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d" ) response = client.post( "/reset/" + token, data={"password": "newpassword", "password_confirm": "newpassword"}, follow_redirects=True, ) assert get_message("INVALID_RESET_PASSWORD_TOKEN") in response.data def test_reset_token_deleted_user(app, client, get_message): with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="gene@lp.com"), follow_redirects=True) token = requests[0]["token"] # Delete user with app.app_context(): # load user (and role) to get into session so cascade delete works. user = app.security.datastore.find_user(email="gene@lp.com") app.security.datastore.delete(user) app.security.datastore.commit() response = client.post( "/reset/" + token, data={"password": "newpassword", "password_confirm": "newpassword"}, follow_redirects=True, ) msg = get_message("INVALID_RESET_PASSWORD_TOKEN") assert msg in response.data def test_used_reset_token(client, get_message): with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="joe@lp.com"), follow_redirects=True) token = requests[0]["token"] # use the token response = client.post( "/reset/" + token, data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, ) assert get_message("PASSWORD_RESET_NO_LOGIN") in response.data logout(client) # attempt to use it a second time response2 = client.post( "/reset/" + token, data={"password": "otherpassword", "password_confirm": "otherpassword"}, follow_redirects=True, ) msg = get_message("INVALID_RESET_PASSWORD_TOKEN") assert msg in response2.data def test_reset_passwordless_user(client, get_message): with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="jess@lp.com"), follow_redirects=True) token = requests[0]["token"] # use the token response = client.post( "/reset/" + token, data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, ) assert get_message("PASSWORD_RESET_NO_LOGIN") in response.data @pytest.mark.settings(reset_url="/custom_reset") def test_custom_reset_url(client): response = client.get("/custom_reset") assert response.status_code == 200 @pytest.mark.settings( reset_password_template="custom_security/reset_password.html", forgot_password_template="custom_security/forgot_password.html", ) def test_custom_reset_templates(client): response = client.get("/reset") assert b"CUSTOM FORGOT PASSWORD" in response.data with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="joe@lp.com"), follow_redirects=True) token = requests[0]["token"] response = client.get("/reset/" + token) assert b"CUSTOM RESET PASSWORD" in response.data @pytest.mark.settings( redirect_host="myui.com:8090", redirect_behavior="spa", reset_view="/reset-redirect", ) def test_spa_get(app, client): """ Test 'single-page-application' style redirects This uses json only. """ with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="joe@lp.com"), ) assert response.headers["Content-Type"] == "application/json" assert "user" not in response.json["response"] token = requests[0]["token"] response = client.get("/reset/" + token) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "myui.com:8090" == split.netloc assert "/reset-redirect" == split.path qparams = dict(parse_qsl(split.query)) # we shouldn't be showing PII assert "email" not in qparams assert qparams["token"] == token @pytest.mark.settings( reset_password_within="1 milliseconds", redirect_host="localhost:8081", redirect_behavior="spa", reset_error_view="/reset-error", ) def test_spa_get_bad_token(app, client, get_message): """Test expired and invalid token""" with capture_flashes() as flashes: # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="joe@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.headers["Content-Type"] == "application/json" assert "user" not in response.json["response"] token = requests[0]["token"] response = client.get("/reset/" + token) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/reset-error" == split.path qparams = dict(parse_qsl(split.query)) # on error - no PII should be returned. assert "error" in qparams assert "identity" not in qparams assert "email" not in qparams msg = get_message( "PASSWORD_RESET_EXPIRED", within="1 milliseconds", email="joe@lp.com" ) assert msg == qparams["error"].encode("utf-8") # Test mangled token token = ( "WyIxNjQ2MzYiLCIxMzQ1YzBlZmVhM2VhZjYwODgwMDhhZGU2YzU0MzZjMiJd." "BZEw_Q.lQyo3npdPZtcJ_sNHVHP103syjM" "&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d" ) response = client.get("/reset/" + token) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/reset-error" == split.path qparams = dict(parse_qsl(split.query)) assert len(qparams) == 1 assert all(k in qparams for k in ["error"]) msg = get_message("INVALID_RESET_PASSWORD_TOKEN") assert msg == qparams["error"].encode("utf-8") assert len(flashes) == 0 @pytest.mark.settings(password_complexity_checker="zxcvbn") def test_easy_password(client, get_message): with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="joe@lp.com"), follow_redirects=True) token = requests[0]["token"] # use the token response = client.post( "/reset/" + token, data={"password": "mypassword", "password_confirm": "mypassword"}, follow_redirects=True, ) assert b"This is a very common password" in response.data def test_reset_inactive(client, get_message): response = client.post( "/reset", data=dict(email="tiya@lp.com"), follow_redirects=True ) assert get_message("DISABLED_ACCOUNT") in response.data response = client.post( "/reset", json=dict(email="tiya@lp.com"), 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 def test_password_normalization(app, client, get_message): with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="matt@lp.com"), ) assert response.status_code == 200 token = requests[0]["token"] response = client.post( "/reset/" + token, json=dict(password="HöheHöhe", password_confirm="HöheHöhe"), ) assert response.status_code == 200 logout(client) # make sure can log in with new password both normalized or not response = client.post( "/login", json=dict(email="matt@lp.com", password="HöheHöhe"), ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 logout(client) response = client.post( "/login", json=dict(email="matt@lp.com", password="Ho\u0308heHo\u0308he"), ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(return_generic_responses=True) def test_generic_response(app, client, get_message): # try unknown user response = client.post("/reset", data=dict(email="whoami@test.com")) assert ( get_message("PASSWORD_RESET_REQUEST", email="whoami@test.com") in response.data ) response = client.post("/reset", json=dict(email="whoami@test.com")) assert response.status_code == 200 assert not any(e in response.json["response"].keys() for e in ["error", "errors"]) def test_generic_with_extra(app, sqlalchemy_datastore): # If application adds a field, make sure we properly return errors # even if 'RETURN_GENERIC_RESPONSES' is set. class MyForgotPasswordForm(ForgotPasswordForm): recaptcha = StringField("Recaptcha", validators=[Length(min=5)]) app.config["SECURITY_RETURN_GENERIC_RESPONSES"] = True app.config["SECURITY_FORGOT_PASSWORD_TEMPLATE"] = "generic_reset.html" app.security = Security( app, datastore=sqlalchemy_datastore, forgot_password_form=MyForgotPasswordForm, ) populate_data(app) client = app.test_client() # Test valid user but invalid additional form field # We should get a form error for the extra (invalid) field, no flash bad_data = dict(email="joe@lp.com", recaptcha="1234") good_data = dict(email="joe@lp.com", recaptcha="123456") with capture_flashes() as flashes: response = client.post("/reset", data=bad_data) assert b"Field must be at least 5" in response.data assert len(flashes) == 0 with capture_flashes() as flashes: response = client.post("/reset", data=good_data) assert len(flashes) == 1 # JSON with capture_flashes() as flashes: response = client.post("/reset", json=bad_data) assert response.status_code == 400 assert ( "Field must be at least 5" in response.json["response"]["field_errors"]["recaptcha"][0] ) assert len(flashes) == 0 with capture_flashes() as flashes: response = client.post("/reset", json=good_data) assert response.status_code == 200 assert len(flashes) == 0 # Try bad email AND bad recaptcha bad_data = dict(email="joe44-lp.com", recaptcha="1234") with capture_flashes() as flashes: response = client.post("/reset", data=bad_data) assert b"Field must be at least 5" in response.data assert len(flashes) == 0 with capture_flashes() as flashes: response = client.post("/reset", json=bad_data) assert response.status_code == 400 assert ( "Field must be at least 5" in response.json["response"]["field_errors"]["recaptcha"][0] ) assert len(response.json["response"]["errors"]) == 1 assert len(flashes) == 0 @pytest.mark.filterwarnings("ignore") @pytest.mark.settings(auto_login_after_reset=True, post_reset_view="/post_reset") def test_auto_login(client, get_message): # test backwards compat flag (not OWASP recommended) with capture_reset_password_requests() as requests: response = client.post( "/reset", data=dict(email="joe@lp.com"), follow_redirects=True ) assert response.status_code == 200 token = requests[0]["token"] # Test submitting a new password with capture_flashes() as flashes: response = client.post( "/reset/" + token, data=dict(password="awesome sunset", password_confirm="awesome sunset"), follow_redirects=True, ) assert b"Post Reset" in response.data assert len(flashes) == 1 assert get_message("PASSWORD_RESET") == flashes[0]["message"].encode("utf-8") # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.filterwarnings("ignore") @pytest.mark.settings(auto_login_after_reset=True) def test_auto_login_json(client, get_message): # test backwards compat flag (not OWASP recommended) with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="joe@lp.com"), ) 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"), ) assert all( k in response.json["response"]["user"] for k in ["email", "authentication_token"] ) # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.flask_async() @pytest.mark.settings() def test_recoverable_json_async(app, client, get_message): recorded_resets = [] recorded_instructions_sent = [] @password_reset.connect_via(app) async def on_password_reset(myapp, user): recorded_resets.append(user) @reset_password_instructions_sent.connect_via(app) async def on_instructions_sent(myapp, **kwargs): recorded_instructions_sent.append(kwargs["user"]) # Test reset password creates a token and sends email with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="joe@lp.com"), headers={"Content-Type": "application/json"}, ) assert len(recorded_instructions_sent) == 1 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"), ) assert not response.json["response"] assert len(recorded_resets) == 1 @pytest.mark.csrf() @pytest.mark.settings(post_reset_view="/post_reset_view") def test_csrf(app, client, get_message): response = client.get("/reset") csrf_token = get_form_input_value(response, "csrf_token") with capture_reset_password_requests() as requests: client.post( "/reset", data=dict(email="joe@lp.com", csrf_token=csrf_token), follow_redirects=True, ) token = requests[0]["token"] # use the token - no CSRF so shouldn't work data = {"password": "mypassword", "password_confirm": "mypassword"} response = client.post( "/reset/" + token, data=data, ) assert b"The CSRF token is missing" in response.data data["csrf_token"] = csrf_token response = client.post(f"/reset/{token}", data=data) assert check_location(app, response.location, "/post_reset_view") @pytest.mark.csrf(ignore_unauth=True) @pytest.mark.settings(post_reset_view="/post_reset_view") def test_auth_csrf(app, client, get_message): # Test reset when authenticated and unauth CSRF is off authenticate(client) response = client.get("/reset") csrf_token = get_form_input_value(response, "csrf_token") assert "matt@lp.com" == get_form_input_value(response, "email") with capture_reset_password_requests() as requests: client.post( "/reset", data=dict(email="matt@lp.com", csrf_token=csrf_token), follow_redirects=True, ) token = requests[0]["token"] # use the token - no CSRF so shouldn't work data = {"password": "mypassword", "password_confirm": "mypassword"} response = client.post( "/reset/" + token, data=data, ) assert b"The CSRF token is missing" in response.data data["csrf_token"] = csrf_token response = client.post(f"/reset/{token}", data=data) assert check_location(app, response.location, "/post_reset_view") assert not is_authenticated(client, get_message) def test_recoverable_auth_json(app, client, get_message, outbox): recorded_resets = [] recorded_instructions_sent = [] @password_reset.connect_via(app) def on_password_reset(app, user): recorded_resets.append(user) @reset_password_instructions_sent.connect_via(app) def on_instructions_sent(app, **kwargs): recorded_instructions_sent.append(kwargs["user"]) authenticate(client, "joe@lp.com", password="password") with capture_flashes() as flashes: # Test reset password creates a token and sends email with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="joe@lp.com"), ) assert len(recorded_instructions_sent) == 1 assert len(outbox) == 1 assert response.status_code == 200 token = requests[0]["token"] # Test invalid email response = client.post( "/reset", json=dict(email="whoknows@lp.com"), ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "USER_DOES_NOT_EXIST" ) # Test submitting a new password response = client.post( "/reset/" + token + "?include_auth_token", json=dict(password="awesome sunset", password_confirm="awesome sunset"), ) assert not response.json["response"] assert len(recorded_resets) == 1 assert not is_authenticated(client, get_message) # Test logging in with the new password response = client.post( "/login?include_auth_token", json=dict(email="joe@lp.com", password="awesome sunset"), ) assert all( k in response.json["response"]["user"] for k in ["email", "authentication_token"] ) logout(client) # Use token again - should fail since already have set new password. response = client.post( "/reset/" + token, json=dict(password="newpassword", password_confirm="newpassword"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 assert len(recorded_resets) == 1 # Test invalid token response = client.post( "/reset/bogus", json=dict(password="newpassword", password_confirm="newpassword"), headers={"Content-Type": "application/json"}, ) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_RESET_PASSWORD_TOKEN" ) assert len(flashes) == 0 @pytest.mark.username_recovery() def test_username_recovery_valid_email(app, clients, get_message, outbox): recorded_recovery_sent = [] @username_recovery_email_sent.connect_via(app) def on_email_sent(app, **kwargs): assert isinstance(app, Flask) assert isinstance(kwargs["user"], UserMixin) recorded_recovery_sent.append(kwargs["user"]) # Test the username recovery view response = clients.get("/recover-username") assert b"

Username Recovery

" in response.data response = clients.post( "/recover-username", data=dict(email="joe@lp.com"), follow_redirects=True ) assert len(recorded_recovery_sent) == 1 assert len(outbox) == 1 assert response.status_code == 200 with capture_flashes() as flashes: response = clients.post( "/recover-username", data=dict(email="joe@lp.com"), follow_redirects=True, ) assert len(flashes) == 1 assert get_message("USERNAME_RECOVERY_REQUEST") == flashes[0]["message"].encode( "utf-8" ) # Validate the emailed username email = outbox[1] assert "Your username is: joe" in email.body # Test JSON responses response = clients.post( "/recover-username", json=dict(email="joe@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/json" @pytest.mark.username_recovery() def test_username_recovery_invalid_email(app, clients, outbox): response = clients.post( "/recover-username", data=dict(email="bogus@lp.com"), follow_redirects=True ) assert len(outbox) == 0 assert response.status_code == 200 # Test JSON responses response = clients.post( "/recover-username", json=dict(email="bogus@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 assert response.headers["Content-Type"] == "application/json" assert len(response.json["response"]["errors"]) == 1 assert ( "Specified user does not exist" in response.json["response"]["field_errors"]["email"][0] ) @pytest.mark.username_recovery() @pytest.mark.settings(return_generic_responses=True) def test_username_recovery_generic_responses(app, clients, get_message, outbox): recorded_recovery_sent = [] @username_recovery_email_sent.connect_via(app) def on_email_sent(app, **kwargs): recorded_recovery_sent.append(kwargs["user"]) # Test with valid email with capture_flashes() as flashes: response = clients.post( "/recover-username", data=dict(email="joe@lp.com"), follow_redirects=True, ) assert len(flashes) == 1 assert get_message("USERNAME_RECOVERY_REQUEST") == flashes[0]["message"].encode( "utf-8" ) assert len(recorded_recovery_sent) == 1 assert len(outbox) == 1 assert response.status_code == 200 # Test with non-existant email (should still return 200) with capture_flashes() as flashes: response = clients.post( "/recover-username", data=dict(email="bogus@lp.com"), follow_redirects=True, ) assert len(flashes) == 1 assert get_message("USERNAME_RECOVERY_REQUEST") == flashes[0]["message"].encode( "utf-8" ) # Validate no email was sent (there should only be one from the previous test) assert len(recorded_recovery_sent) == 1 assert len(outbox) == 1 assert response.status_code == 200 # Test JSON responses - valid email response = clients.post( "/recover-username", json=dict(email="joe@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/json" # Test JSON responses - invalid email response = clients.post( "/recover-username", json=dict(email="bogus@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/json" assert not any(e in response.json["response"].keys() for e in ["error", "errors"]) flask-security-5.7.1/tests/test_recovery_codes.py000066400000000000000000000304031511046741400222620ustar00rootroot00000000000000""" test_recovery_codes ~~~~~~~~~~~~~~~~~ recovery code tests :copyright: (c) 2022-2024 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import re import pytest from tests.test_two_factor import tf_authenticate from tests.test_utils import ( authenticate, logout, reset_fresh, setup_tf_sms, ) pytestmark = pytest.mark.two_factor() @pytest.mark.settings(multi_factor_recovery_codes=True) def test_rc_json(app, clients, get_message): # Test recovery codes # gal has two-factor already setup for 'sms' client = clients headers = {"Accept": "application/json", "Content-Type": "application/json"} tf_authenticate(app, client) response = client.get("/mf-recovery-codes", headers=headers) assert response.status_code == 200 codes = response.json["response"]["recovery_codes"] assert len(codes) == 0 response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 response = client.get("/mf-recovery-codes", headers=headers) recodes = response.json["response"]["recovery_codes"] assert len(recodes) == 5 and codes[0] == recodes[0] response = client.get("/mf-recovery-codes", headers=headers) assert response.status_code == 200 assert not hasattr(response.json["response"], "recovery_codes") # endpoint is for unauthenticated only response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 400 logout(client) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # logout and try again - first code shouldn't work again logout(client) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_RECOVERY_CODE" ) response = client.post("/mf-recovery", json=dict(code=codes[1])) assert response.status_code == 200 @pytest.mark.settings(multi_factor_recovery_codes=True) def test_rc(app, client, get_message): # Test recovery codes # gal has two-factor already setup for 'sms' tf_authenticate(app, client) response = client.get("/mf-recovery-codes?show_codes=hi") assert response.status_code == 200 assert get_message("NO_RECOVERY_CODES_SETUP") in response.data response = client.post("/mf-recovery-codes") rd = response.data.decode("utf-8") codes = re.findall(r"[a-f,\d]{4}-[a-f,\d]{4}-[a-f,\d]{4}", rd) assert len(codes) == 5 response = client.get("/mf-recovery-codes?show_codes=hi") assert response.status_code == 200 assert b"Recovery Codes" in response.data rd = response.data.decode("utf-8") codes = re.findall(r"[a-f,\d]{4}-[a-f,\d]{4}-[a-f,\d]{4}", rd) assert len(codes) == 5 # endpoint is for unauthenticated only response = client.post( "/mf-recovery", data=dict(code=codes[0]), follow_redirects=False ) assert response.status_code == 302 logout(client) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post( "/mf-recovery", data=dict(code=codes[0]), follow_redirects=True ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # logout and try again - first code shouldn't work again logout(client) response = client.post("/login", data=dict(email="gal@lp.com", password="password")) response = client.post("/mf-recovery", data=dict(code=codes[0])) assert get_message("INVALID_RECOVERY_CODE") in response.data response = client.post( "/mf-recovery", data=dict(code=codes[1]), follow_redirects=True ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(multi_factor_recovery_codes=True) def test_rc_reset(app, client, get_message): # test that reset_user_access, removes recovery codes headers = {"Accept": "application/json", "Content-Type": "application/json"} tf_authenticate(app, client) response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 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() client.post("/login", json=dict(email="gal@lp.com", password="password")) response = client.get("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 0 @pytest.mark.settings(multi_factor_recovery_codes=True, url_prefix="/api") def test_rc_bad_state(app, client, get_message): response = client.post("/api/mf-recovery", json=dict(code="hi")) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf=8") == get_message( "TWO_FACTOR_PERMISSION_DENIED" ) response = client.post( "/api/mf-recovery", data=dict(code="hi"), follow_redirects=False ) assert response.status_code == 302 assert "/api/login" in response.location @pytest.mark.settings(multi_factor_recovery_codes=True) def test_rc_rescue(app, client, get_message): headers = {"Accept": "application/json", "Content-Type": "application/json"} tf_authenticate(app, client) response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 logout(client) data = dict(email="gal@lp.com", password="password") response = client.post("/login", json=data) assert response.json["response"]["tf_required"] response = client.get("/tf-rescue") assert b"Use previously downloaded recovery code" in response.data response = client.get("/tf-rescue", headers=headers) options = response.json["response"]["recovery_options"] assert "recovery_code" in options.keys() assert "/mf-recovery" in options["recovery_code"] response = client.post( "/tf-rescue", data=dict(help_setup="recovery_code"), follow_redirects=False ) assert "/mf-recovery" in response.location @pytest.mark.settings(multi_factor_recovery_codes=True) def test_fresh(app, client): # Make sure fetching recovery codes is protected with a freshness check. headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client) response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.post("/mf-recovery-codes", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] response = client.post("/verify", json=dict(password="password")) assert response.status_code == 200 response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 @pytest.mark.settings( multi_factor_recovery_codes=True, multi_factor_recovery_codes_keys=[b"6_WhDTwI1RKJ3_ra9nADCBQbrRywlA88psGsq21xcsU="], ) def test_rc_json_encrypted(app, client, get_message): # Test recovery codes with encryption option # gal has two-factor already setup for 'sms' headers = {"Accept": "application/json", "Content-Type": "application/json"} tf_authenticate(app, client) response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 # endpoint is for unauthenticated only response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 400 logout(client) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # logout and try again - first code shouldn't work again logout(client) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_RECOVERY_CODE" ) response = client.post("/mf-recovery", json=dict(code=codes[1])) assert response.status_code == 200 # make sure DB actually has encrypted codes! with app.test_request_context("/"): user = app.security.datastore.find_user(email="gal@lp.com") codes = user.mf_recovery_codes assert len(codes) == 3 assert len(codes[0]) == 100 @pytest.mark.settings( multi_factor_recovery_codes=True, multi_factor_recovery_codes_keys=[b"6_WhDTwI1RKJ3_ra9nADCBQbrRywlA88psGsq21xcsU="], ) def test_rc_json_encrypted_multi(app, client, get_message): # Test recovery codes with encryption option and multiple keys # gal has two-factor already setup for 'sms' from cryptography.fernet import Fernet headers = {"Accept": "application/json", "Content-Type": "application/json"} tf_authenticate(app, client) response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 logout(client) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 logout(client) # add a new key to cryptor and verify old code still works key2 = Fernet.generate_key() newkeys = [key2, b"6_WhDTwI1RKJ3_ra9nADCBQbrRywlA88psGsq21xcsU="] app.security.mf_recovery_codes_util.setup_cryptor(newkeys) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[1])) assert response.status_code == 200 logout(client) # creat codes for new user - this should use the new primary key. response = client.post( "/login", json=dict(email="matt@lp.com", password="password") ) setup_tf_sms(client) response = client.post("/mf-recovery-codes", headers=headers) matt_codes = response.json["response"]["recovery_codes"] logout(client) # Now remove older key and codes for gal@lp.com shouldn't work app.security._mf_recovery_codes_util.setup_cryptor([key2]) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[2])) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_RECOVERY_CODE" ) # matts codes should work response = client.post( "/login", json=dict(email="matt@lp.com", password="password") ) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=matt_codes[0])) assert response.status_code == 200 flask-security-5.7.1/tests/test_registerable.py000066400000000000000000001240571511046741400217300ustar00rootroot00000000000000""" test_registerable ~~~~~~~~~~~~~~~~~ Registerable tests """ import pytest import re from flask import Flask from tests.conftest import v2_param from tests.test_utils import ( authenticate, check_xlation, get_form_input_value, get_form_input, init_app_with_options, json_authenticate, logout, is_authenticated, ) from flask_security import Security, UserMixin, user_registered, user_not_registered from flask_security.forms import ( ConfirmRegisterForm, RegisterForm, RegisterFormV2, StringField, _default_field_labels, ) from flask_security.utils import localize_callback pytestmark = pytest.mark.registerable() @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.settings(post_register_view="/post_register") def test_registerable_flag(clients, app, get_message, outbox): recorded = [] # Test the register view response = clients.get("/register") assert b"

Register

" in response.data assert re.search(b']*type="email"[^>]*>', response.data) assert get_form_input_value(response, "email") is not None assert not get_form_input_value(response, "username") # Test registering is successful, sends email, and fires signal @user_registered.connect_via(app) def on_user_registered(app, **kwargs): assert isinstance(app, Flask) assert isinstance(kwargs["user"], UserMixin) assert kwargs["confirm_token"] is None assert len(kwargs["form_data"].keys()) > 0 recorded.append(kwargs["user"]) data = dict( email="dude@lp.com", password="battery staple", password_confirm="battery staple", next="", ) 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(clients) # Test user can login after registering response = authenticate(clients, email="dude@lp.com", password="battery staple") assert response.status_code == 302 logout(clients) # Test registering with an existing email data = dict( email="dude@lp.com", password="password", password_confirm="password", next="" ) 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 = 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", password_confirm="horse battery" ) response = clients.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.headers["content-type"] == "application/json" assert response.json["meta"]["code"] == 200 assert len(response.json["response"]) == 2 assert all(k in response.json["response"] for k in ["csrf_token", "user"]) logout(clients) # Test registering with invalid JSON data = dict(email="bogus", password="password", password_confirm="password") 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(clients) # Test ?next param data = dict( email="dude3@lp.com", password="horse staple", password_confirm="horse staple", next="", ) response = clients.post("/register?next=/page1", data=data, follow_redirects=True) assert b"Page 1" in response.data @pytest.mark.csrf(csrfprotect=True) def test_form_csrf(app, client): # Test that CSRFprotect config catches CSRF requirement at unauth_csrf() decorator. # Note that in this case - 400 is returned - though if CSRFprotect isn't enabled # then CSRF errors are caught at form.submit time and a 200 with error values # is returned. response = client.post( "/register", data=dict( email="csrf@example.com", password="mypassword", password_confirm="mypassword", ), ) assert response.status_code == 400 assert b"The CSRF token is missing" in response.data response = client.get("/register") csrf_token = get_form_input_value(response, "csrf_token") response = client.post( "/register", data=dict( email="csrf@example.com", password="mypassword", password_confirm="mypassword", csrf_token=csrf_token, ), follow_redirects=False, ) assert response.status_code == 302 assert response.location == "/" @pytest.mark.confirmable() @pytest.mark.app_settings(babel_default_locale="fr_FR") @pytest.mark.babel() def test_xlation(app, client, get_message_local, outbox): # Test form and email translation assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" confirmation_token = [] @user_registered.connect_via(app) def on_user_registered(app, **kwargs): confirmation_token.append(kwargs["confirmation_token"]) response = client.get("/register", follow_redirects=True) with app.test_request_context(): # Check header assert f'

{localize_callback("Register")}

'.encode() in response.data submit = localize_callback(_default_field_labels["register"]) assert f'value="{submit}"'.encode() in response.data response = client.post( "/register", data={ "email": "me@fr.com", "password": "new strong password", "password_confirm": "new strong password", }, follow_redirects=True, ) with app.test_request_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 ) lc = localize_callback( 'Use this link to confirm your email' " address.", confirmation_link=f"http://localhost/confirm/{confirmation_token[0]}", ) assert lc in outbox[0].alts["html"] @pytest.mark.confirmable() @pytest.mark.settings(use_register_v2=False) def test_required_password(client, get_message): # when confirm required - should not require confirm_password - but should # require a password data = dict(email="trp@lp.com", password="") response = client.post("/register", data=data, follow_redirects=True) assert get_message("PASSWORD_NOT_PROVIDED") in response.data data = dict(email="trp@lp.com", password="battery staple") response = client.post("/register", data=data, follow_redirects=True) assert get_message("CONFIRM_REGISTRATION", email="trp@lp.com") in response.data @pytest.mark.settings(use_register_v2=False) def test_required_password_confirm(client, get_message): response = client.post( "/register", data={ "email": "trp@lp.com", "password": "password", "password_confirm": "notpassword", }, follow_redirects=True, ) assert get_message("RETYPE_PASSWORD_MISMATCH") in response.data response = client.post( "/register", data={"email": "trp@lp.com", "password": "password", "password_confirm": ""}, follow_redirects=True, ) assert get_message("PASSWORD_NOT_PROVIDED") in response.data @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.unified_signin() @pytest.mark.confirmable() @pytest.mark.settings(password_required=False) def test_allow_null_password(app, client, get_message): # Test password not required with either register form response = client.get("/register") data = dict(email="trp@lp.com") pw = get_form_input(response, "password") assert "required" not in pw pwc = get_form_input(response, "password_confirm") assert not pwc or "required" not in pwc response = client.post("/register", data=data, follow_redirects=True) assert get_message("CONFIRM_REGISTRATION", email="trp@lp.com") in response.data logout(client) # should be able to register with password and password_confirm (with RegisterFormV2 if app.config["SECURITY_USE_REGISTER_V2"]: data = dict( email="trp2@lp.com", password="battery staple", password_confirm="battery staples", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("RETYPE_PASSWORD_MISMATCH") in response.data data = dict( email="trp2@lp.com", password="battery staple", password_confirm="battery staple", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("CONFIRM_REGISTRATION", email="trp2@lp.com") in response.data @pytest.mark.unified_signin() @pytest.mark.settings(password_required=False, use_register_v2=False) def test_allow_null_password_nologin(client, get_message): # If unified sign in is enabled - should be able to register w/o password # With confirmable false - should be logged in automatically upon register. # But shouldn't be able to perform normal login again data = dict(email="trp@lp.com", password="") response = client.post("/register", data=data, follow_redirects=True) assert b"Welcome trp@lp.com" in response.data logout(client) # Make sure can't log in response = authenticate(client, email="trp@lp.com", password="") assert get_message("PASSWORD_NOT_PROVIDED") in response.data response = authenticate(client, email="trp@lp.com", password="NoPassword") assert get_message("INVALID_PASSWORD") in response.data @pytest.mark.settings( register_url="/custom_register", post_register_view="/post_register" ) def test_custom_register_url(client): response = client.get("/custom_register") assert b"

Register

" in response.data data = dict( email="dude@lp.com", password="battery staple", password_confirm="battery staple", next="", ) response = client.post("/custom_register", data=data, follow_redirects=True) assert b"Post Register" in response.data @pytest.mark.settings(register_user_template="custom_security/register_user.html") def test_custom_register_template(client): response = client.get("/register") assert b"CUSTOM REGISTER USER" in response.data @pytest.mark.settings(send_register_email=False) def test_disable_register_emails(client, app, outbox): data = dict( email="dude@lp.com", password="password", password_confirm="password", next="" ) client.post("/register", data=data, follow_redirects=True) assert len(outbox) == 0 @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 2-factor setup. """ data = dict(email="dude@lp.com", password="password", password_confirm="password") response = client.post("/register", data=data, follow_redirects=False) assert "tf-setup" in response.location # make sure not logged in response = client.get("/profile") assert response.status_code == 302 assert response.location == "/login?next=/profile" @pytest.mark.two_factor() @pytest.mark.settings(two_factor_required=True) def test_two_factor_json(app, client, get_message): data = dict(email="dude@lp.com", password="password", password_confirm="password") response = client.post("/register", content_type="application/json", json=data) assert response.status_code == 200 assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "setup_from_login" # make sure not logged in response = client.get("/profile", headers={"accept": "application/json"}) assert response.status_code == 401 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "UNAUTHENTICATED" ) def test_form_data_is_passed_to_user_registered_signal(app, sqlalchemy_datastore): class MyRegisterForm(RegisterFormV2): additional_field = StringField("additional_field") app.security = Security( app, datastore=sqlalchemy_datastore, register_form=MyRegisterForm ) recorded = [] @user_registered.connect_via(app) def on_user_registered(app, **kwargs): assert isinstance(app, Flask) assert isinstance(kwargs["user"], UserMixin) assert kwargs["confirm_token"] is None assert kwargs["confirmation_token"] is None assert kwargs["form_data"]["additional_field"] == "additional_data" recorded.append(kwargs["user"]) client = app.test_client() data = dict( email="dude@lp.com", password="password", password_confirm="password", additional_field="additional_data", ) response = client.post("/register", data=data, follow_redirects=True) assert response.status_code == 200 assert len(recorded) == 1 @pytest.mark.settings(password_complexity_checker="zxcvbn") @pytest.mark.filterwarnings("ignore:.*The ConfirmRegisterForm.*:DeprecationWarning") def test_easy_password(app, sqlalchemy_datastore): class MyRegisterForm(ConfirmRegisterForm): username = StringField("Username") app.config["SECURITY_CONFIRM_REGISTER_FORM"] = MyRegisterForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() # With zxcvbn data = dict( email="dude@lp.com", username="dude", password="mattmatt2", password_confirm="mattmatt2", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 400 # Response from zxcvbn assert "Repeats like" in response.json["response"]["errors"][0] # Test that username affects password selection data = dict( email="dude@lp.com", username="Joe", password="JoeTheDude", password_confirm="JoeTheDude", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 400 # Response from zxcvbn assert ( "Password not complex enough" in response.json["response"]["field_errors"]["password"][0] ) @pytest.mark.settings(username_enable=True, password_confirm_required=False) def test_nullable_username(app, client): # sqlalchemy datastore uses fsqlav2 which has username as unique and nullable # make sure can register multiple users with no username # Note that current WTForms (2.2.1) has a bug where StringFields can never be # None - it changes them to an empty string. DBs don't like that if you have # your column be 'nullable'. data = dict(email="u1@test.com", password="password") response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 200 logout(client) data = dict(email="u2@test.com", password="password") response = client.post( "/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 or canonical unicode form data = dict( email="Imnumber\N{OHM SIGN}@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="Imnumber\N{OHM SIGN}@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="Imnumber\N{OHM SIGN}@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="Imnumber\N{GREEK CAPITAL LETTER OMEGA}@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 @pytest.mark.babel() def test_form_error(app, client, get_message): # A few form validations use ValidatorMixin which provides a lazy string # Since CLI doesn't render_template it was seeing those lazy strings. # This test basically just illustrates all this. from babel.support import LazyProxy with app.test_request_context("/register"): rform = ConfirmRegisterForm() rform.validate() assert isinstance(rform.errors["email"][0], LazyProxy) assert str(rform.errors["email"][0]).encode("utf-8") == get_message( "EMAIL_NOT_PROVIDED" ) # make sure rendered template has converted LocalProxy strings. rendered = app.security.render_template( app.config["SECURITY_REGISTER_USER_TEMPLATE"], register_user_form=rform, ) assert get_message("EMAIL_NOT_PROVIDED") in rendered.encode("utf-8") @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.settings(username_enable=True, password_confirm_required=False) @pytest.mark.unified_signin() def test_username(app, clients, get_message): client = clients data = dict( email="dude@lp.com", username="dude", password="awesome sunset", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 200 logout(client) # login using historic - email field to hold a username - won't work since # it is an EmailField response = json_authenticate(client, email="dude", password="awesome sunset") assert response.status_code == 400 assert ( get_message("INVALID_EMAIL_ADDRESS", username="dude") == response.json["response"]["errors"][0].encode() ) # login using username response = client.post( "/login", json=dict(username="dude", password="awesome sunset") ) assert response.status_code == 200 assert is_authenticated(client, get_message) logout(client) # login with email response = client.post( "/login", json=dict(email="dude@lp.com", password="awesome sunset") ) assert response.status_code == 200 assert is_authenticated(client, get_message) logout(client) response = client.post( "/login", json=dict(emails="dude", password="awesome sunset") ) assert response.status_code == 400 assert ( get_message("USER_DOES_NOT_EXIST") == response.json["response"]["field_errors"][""][0].encode() ) # login using us-signin response = client.post( "/us-signin", data=dict(identity="dude", passcode="awesome sunset"), follow_redirects=True, ) assert b"Welcome dude@lp.com" in response.data # make sure username is unique logout(client) data = dict( email="dude@lp.com", username="dude", password="awesome sunset", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert ( get_message("USERNAME_ALREADY_ASSOCIATED", username="dude") == response.json["response"]["field_errors"]["username"][0].encode() ) @pytest.mark.settings(username_enable=True) def test_username_not_set(app, client, get_message): # login with null username - shouldn't match # note that this is caught at LoginForm - it doesn't force a DB call response = client.post( "/register", json=dict( email="justemail@lp.com", username="", password="awesome sunset", password_confirm="awesome sunset", ), ) assert response.status_code == 200 client.post("/logout") response = client.post("/login", json=dict(username="", password="awesome sunset")) assert response.status_code == 400 assert ( get_message("USER_DOES_NOT_EXIST") == response.json["response"]["field_errors"][""][0].encode() ) @pytest.mark.settings(username_enable=True) def test_username_template(app, client): # verify template displays username option response = client.get("/register") username_field = get_form_input_value(response, "username") assert username_field is not None @pytest.mark.settings(username_enable=True) @pytest.mark.unified_signin() def test_username_normalize(app, client, get_message): data = dict( email="dude@lp.com", username="Imnumber\N{ROMAN NUMERAL ONE}", password="awesome sunset", password_confirm="awesome sunset", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 200 logout(client) response = client.post( "/us-signin", data=dict( identity="Imnumber\N{LATIN CAPITAL LETTER I}", passcode="awesome sunset" ), follow_redirects=True, ) assert b"Welcome dude@lp.com" in response.data @pytest.mark.settings(username_enable=True, username_required=True) def test_username_errors(app, client, get_message): data = dict( email="dude@lp.com", username="dud", password="awesome sunset", password_confirm="awesome sunset", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert "Username must be at least" in response.json["response"]["errors"][0] data["username"] = "howlongcanIbebeforeyoucomplainIsupposereallyreallylong" response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert "Username must be at least" in response.json["response"]["errors"][0] # try evil characters data["username"] = "hi " response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert ( get_message("USERNAME_ILLEGAL_CHARACTERS") == response.json["response"]["field_errors"]["username"][0].encode() ) # No spaces or punctuation data["username"] = "hi there?" response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert ( get_message("USERNAME_DISALLOWED_CHARACTERS") in response.json["response"]["errors"][0].encode() ) data["username"] = None response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert ( get_message("USERNAME_NOT_PROVIDED") == response.json["response"]["errors"][0].encode() ) def test_username_not_enabled(app, client, get_message): response = client.get("/register") assert b"username" not in response.data assert not hasattr(RegisterForm, "username") @pytest.mark.filterwarnings( "ignore:.*The RegisterForm is deprecated.*:DeprecationWarning" ) def test_legacy_style_login(app, sqlalchemy_datastore, get_message): # Show how to setup LoginForm to mimic legacy behavior of # allowing any identity in the email field. # N.B. for simplicity we don't enable confirmable.... from flask_security import ( RegisterForm, LoginForm, Security, uia_username_mapper, unique_identity_attribute, ) from flask_security.utils import lookup_identity from werkzeug.local import LocalProxy from wtforms import StringField, ValidationError, validators def username_validator(form, field): # Side-effect - field.data is updated to normalized value. # Use proxy to we can declare this prior to initializing Security. _security = LocalProxy(lambda: app.extensions["security"]) msg, field.data = _security.username_util.validate(field.data) if msg: raise ValidationError(msg) class MyRegisterForm(RegisterForm): # Note that unique_identity_attribute uses the defined field 'mapper' to # normalize. We validate before that to give better error messages and # to set the normalized value into the form for saving. username = StringField( "Username", validators=[ validators.data_required(), username_validator, unique_identity_attribute, ], ) class MyLoginForm(LoginForm): email = StringField("email", [validators.data_required()]) def validate(self, **kwargs): self.user = lookup_identity(self.email.data) self.ifield = self.email if not super().validate(**kwargs): return False return True app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"] = [ {"username": {"mapper": uia_username_mapper}} ] security = Security( datastore=sqlalchemy_datastore, register_form=MyRegisterForm, login_form=MyLoginForm, ) security.init_app(app) client = app.test_client() data = dict( email="mary2@lp.com", username="mary", password="awesome sunset", password_confirm="awesome sunset", ) response = client.post("/register", data=data, follow_redirects=True) assert b"Welcome mary" in response.data logout(client) # log in with username response = client.post( "/login", data=dict(email="mary", password="awesome sunset"), follow_redirects=True, ) # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.confirmable() @pytest.mark.settings( return_generic_responses=True, username_enable=True, password_confirm_required=False ) def test_generic_response(app, client, get_message, outbox): # Register should not expose whether email/username is already in system. recorded = [] @user_not_registered.connect_via(app) def on_user_not_registered(app, **kwargs): recorded.append(kwargs) # register new user data = dict( email="dude@lp.com", username="dude", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 200 assert len(outbox) == 1 assert len(recorded) == 0 # try again - should not get ANY error - but should get an email response = client.post("/register", json=data) assert response.status_code == 200 assert not any( e in response.json["response"].keys() for e in ["errors", "field_errors"] ) # this is using the test template matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) kv = {m.split(":")[0]: m.split(":", 1)[1] for m in matcher} assert kv["User"] == "dude" assert kv["Email"] == "dude@lp.com" assert kv["RegisterBlueprint"] == "True" assert "/confirm" in kv["ConfirmationLink"] assert kv["ConfirmationToken"] assert not kv["ResetLink"] assert not kv["ResetToken"] # verify that signal sent. assert len(recorded) == 1 nr = recorded[0] assert nr["existing_email"] assert nr["user"].email == "dude@lp.com" assert nr["form_data"]["email"] == "dude@lp.com" # Forms should get generic response - even though email already registered. response = client.post("/register", data=data, follow_redirects=True) assert get_message("CONFIRM_REGISTRATION", email="dude@lp.com") in response.data assert len(outbox) == 3 assert len(recorded) == 2 # Try same email with different username response = client.post( "/register", data=dict(email="dude@lp.com", username="dude2", password="awesome sunset"), follow_redirects=True, ) assert get_message("CONFIRM_REGISTRATION", email="dude@lp.com") in response.data assert len(outbox) == 4 assert len(recorded) == 3 matcher = re.findall(r"\w+:.*", outbox[3].body, re.IGNORECASE) kv = {m.split(":")[0]: m.split(":", 1)[1] for m in matcher} assert kv["User"] == "dude" @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.confirmable() @pytest.mark.settings( return_generic_responses=True, username_enable=True, password_confirm_required=False ) def test_gr_existing_username(app, client, get_message, outbox): # Test a new email with an existing username # Should still return errors such as illegal password recorded = [] @user_not_registered.connect_via(app) def on_user_not_registered(app, **kwargs): recorded.append(kwargs) client.post( "/register", json=dict(email="dude@lp.com", username="dude", password="awesome sunset"), ) response = client.post( "/register", data=dict(email="dude39@lp.com", username="dude", password="awesome sunset"), follow_redirects=True, ) assert get_message("CONFIRM_REGISTRATION", email="dude39@lp.com") in response.data assert len(outbox) == 2 assert len(recorded) == 1 gr = outbox[1] assert 'You attempted to register with a username "dude" that' in gr.body # test that signal sent. nr = recorded[0] assert not nr["existing_email"] assert not nr["user"] assert nr["existing_username"] assert nr["form_data"]["username"] == "dude" # should still get detailed errors about e.g. bad password data = dict( email="dude@lp.com", username="dude", password="a", ) response = client.post("/register", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode() == get_message( "PASSWORD_INVALID_LENGTH", length=8 ) data = dict( email="dude@lp.com", username="dd", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode() == get_message( "USERNAME_INVALID_LENGTH", min=4, max=32 ) @pytest.mark.recoverable() @pytest.mark.confirmable() @pytest.mark.settings( return_generic_responses=True, username_enable=True, send_register_email_welcome_existing_template="welcome_existing", password_confirm_required=False, ) def test_gr_extras(app, client, get_message, outbox): # If user tries to re-register - response email should contain reset password # link and (if applicable) a confirmation link data = dict( email="dude@lp.com", username="dude", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 200 assert len(outbox) == 1 # now same email - same or different username data = dict( email="dude@lp.com", username="dude2", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 200 assert len(outbox) == 2 matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) kv = {m.split(":")[0]: m.split(":", 1)[1] for m in matcher} assert kv["User"] == "dude" confirm_link = kv["ConfirmationLink"] response = client.get(confirm_link) assert response.status_code == 302 # now confirmed - should not get confirmation link response = client.post("/register", json=data) assert response.status_code == 200 assert len(outbox) == 3 matcher = re.findall(r"\w+:.*", outbox[2].body, re.IGNORECASE) kv = {m.split(":")[0]: m.split(":", 1)[1] for m in matcher} assert not kv["ConfirmationLink"] assert not kv["ConfirmationToken"] # but should still have reset link - test that reset_link = kv["ResetLink"] response = client.post( reset_link, json=dict(password="awesome password2", password_confirm="awesome password2"), ) assert response.status_code == 200 response = client.post( "/login", json=dict(username="dude", password="awesome password2") ) assert response.status_code == 200 @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.recoverable() @pytest.mark.confirmable() @pytest.mark.settings(return_generic_responses=True, password_confirm_required=False) def test_gr_real_html_template(app, client, get_message, outbox): # We have a test .txt template - but this will use the normal/real HTML template data = dict( email="dude@lp.com", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 200 assert len(outbox) == 1 # now same email - same or different username data = dict( email="dude@lp.com", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 200 assert len(outbox) == 2 matcher = re.findall( r'href="(\S*)"', outbox[1].alts["html"], re.IGNORECASE, ) assert "/reset" in matcher[0] assert "/confirm" in matcher[1] def test_subclass(app, sqlalchemy_datastore): # Test/show how to use multiple inheritance to override individual form fields. from wtforms import PasswordField, ValidationError from wtforms.validators import DataRequired from flask_security.forms import get_form_field_label def password_validator(form, field): if field.data.startswith("PASS"): raise ValidationError("Really - don't start a password with PASS") class NewPasswordFormMixinEx: password = PasswordField( get_form_field_label("password"), validators=[ DataRequired(message="PASSWORD_NOT_PROVIDED"), password_validator, ], ) class MyRegisterForm(NewPasswordFormMixinEx, RegisterFormV2): pass app.config["SECURITY_REGISTER_FORM"] = MyRegisterForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() data = dict( email="dude@lp.com", password="PASSmattmatt", password_confirm="PASSmattmatt", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert "Really - don't start" in response.json["response"]["errors"][0] @pytest.mark.confirmable() @pytest.mark.settings(return_generic_responses=True) def test_my_mail_util(app, sqlalchemy_datastore): # Test that with generic_responses - we still can get syntax/validation errors. from flask_security import MailUtil, EmailValidateException class MyMailUtil(MailUtil): def validate(self, email): if email.startswith("mike"): raise EmailValidateException("No mikes allowed") init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"mail_util_cls": MyMailUtil}} ) data = dict( email="mike@lp.com", password="awesome sunset", ) client = app.test_client() response = client.post("/register", data=data) assert b"No mikes allowed" in response.data @pytest.mark.settings( register_form=RegisterFormV2, post_register_view="/post_register", ) def test_regv2(app, client, get_message): # default config should require password, password_confirm and not allow username response = client.get("/register") assert b"

Register

" in response.data email = get_form_input(response, "email") assert all(s in email for s in ["required", 'type="email"']) pw = get_form_input(response, "password") assert all(s in pw for s in ["required", 'type="password"']) pwc = get_form_input(response, "password_confirm") assert all(s in pwc for s in ["required", 'type="password"']) assert not get_form_input(response, "username") # check password required data = dict( email="dude@lp.com", password="", password_confirm="battery staple", next="", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("PASSWORD_NOT_PROVIDED") in response.data # check confirm required data = dict( email="dude@lp.com", password="battery staple", password_confirm="", next="", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("PASSWORD_NOT_PROVIDED") in response.data # JSON also required password_confirm in V2 response = client.post("/register", json=data) assert ( get_message("PASSWORD_NOT_PROVIDED") == response.json["response"]["errors"][0].encode() ) # check confirm matches data = dict( email="dude@lp.com", password="battery staple", password_confirm="batery staple", next="", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("RETYPE_PASSWORD_MISMATCH") in response.data data = dict( email="dude@lp.com", password="battery staple", password_confirm="battery staple", next="", ) response = client.post("/register", data=data, follow_redirects=True) assert b"Post Register" in response.data @pytest.mark.settings( register_form=RegisterFormV2, post_register_view="/post_register", password_confirm_required=False, ) def test_regv2_no_confirm(app, client, get_message): # Test password confirm not required response = client.get("/register") assert b"

Register

" in response.data email = get_form_input(response, "email") assert all(s in email for s in ["required", 'type="email"']) pw = get_form_input(response, "password") assert all(s in pw for s in ["required", 'type="password"']) assert not get_form_input(response, "password_confirm") assert not get_form_input(response, "username") data = dict( email="dude@lp.com", password="abc", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("PASSWORD_INVALID_LENGTH", length=8) in response.data data = dict( email="dude@lp.com", password="battery staple", password_confirm="battery terminal", # Ignored next="/my_next_idea", ) response = client.post("/register", data=data, follow_redirects=False) assert response.location == "/my_next_idea" @pytest.mark.settings( use_register_v2=True, post_register_view="/post_register", password_confirm_required=False, username_enable=True, username_required=True, ) def test_regv2_no_confirm_username(app, client, get_message): # Test password confirm not required but username is response = client.get("/register") assert b"

Register

" in response.data email = get_form_input(response, "email") assert all(s in email for s in ["required", 'type="email"']) pw = get_form_input(response, "password") assert all(s in pw for s in ["required", 'type="password"']) assert not get_form_input(response, "password_confirm") us = get_form_input(response, "username") assert all(s in us for s in ["required", 'type="text"']) data = dict( email="dude@lp.com", password="battery staple", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("USERNAME_NOT_PROVIDED") in response.data data = dict( email="dude@lp.com", password="battery staple", username="dude", ) response = client.post("/register", data=data, follow_redirects=True) assert b"Post Register" in response.data @pytest.mark.settings( use_register_v2=True, password_confirm_required=False, ) def test_subclass_v2(app, sqlalchemy_datastore): # Test that is create our own RegisterForm the USE_REGISTER_V2 config is ignored class MyRegisterForm(RegisterFormV2): from wtforms.validators import Length myfield = StringField(label="My field", validators=[Length(min=8)]) app.config["SECURITY_REGISTER_FORM"] = MyRegisterForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() data = dict( email="dude@lp.com", password="battery staple", myfield="short", ) response = client.post("/register", json=data) assert ( response.json["response"]["errors"][0] == "Field must be at least 8 characters long." ) flask-security-5.7.1/tests/test_response.py000066400000000000000000000120571511046741400211120ustar00rootroot00000000000000""" test_response ~~~~~~~~~~~~~~~~~ Tests for validating default and plugable responses. :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import pytest from flask import jsonify from tests.test_utils import authenticate def test_render_json(app, client): @app.security.render_json def my_json(payload, code, headers=None, user=None): return jsonify(dict(myresponse=payload, code=code)) response = client.get( "/login", data={}, headers={"Content-Type": "application/json"} ) assert "myresponse" in response.json assert response.json["code"] == 200 def _my_json(payload, code, headers=None, user=None): return jsonify(dict(myresponse=payload, code=code)) def test_render_json2(app, client): app.extensions["security"].render_json(_my_json) response = client.get( "/login", data={}, headers={"Content-Type": "application/json"} ) assert "myresponse" in response.json assert response.json["code"] == 200 def test_render_json_logout(app, client): app.extensions["security"].render_json(_my_json) response = client.post("/logout", headers={"Content-Type": "application/json"}) assert "myresponse" in response.json assert response.json["code"] == 200 def test_default_unauthn(app, client): """Test default unauthn handler with and without json""" response = client.get("/profile") assert response.status_code == 302 assert response.location == "/login?next=/profile" 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 # most browsers intercept it. assert "WWW-Authenticate" not in response.headers @pytest.mark.settings(login_url="/mylogin", url_prefix="/myprefix") def test_default_unauthn_bp(app, client): """Test default unauthn handler with blueprint prefix and login url""" response = client.get("/profile") assert response.status_code == 302 assert response.location == "/myprefix/mylogin?next=/profile" def test_default_unauthn_myjson(app, client): """Make sure render_json gets called for unauthn errors""" @app.security.render_json def my_json(payload, code, headers=None, user=None): return jsonify(dict(myresponse=payload, code=code)), code, headers response = client.get("/multi_auth", headers={"Accept": "application/json"}) assert response.status_code == 401 assert response.json["code"] == 401 assert "myresponse" in response.json def test_my_unauthn_handler(app, client): @app.security.unauthn_handler def my_unauthn(mechanisms, headers=None): return app.security._render_json({"mechanisms": mechanisms}, 401, headers, None) response = client.get("/multi_auth", headers={"Accept": "application/json"}) assert response.status_code == 401 assert all( m in response.json["response"]["mechanisms"] for m in ["session", "token", "basic"] ) def test_default_unauthz(app, client): """Test default unauthz handler with and without json""" authenticate(client, "joe@lp.com", "password") response = client.get("/admin") # This is the result of abort(403) since there is no UNAUTHORIZED_VIEW assert response.status_code == 403 response = client.get("/admin", headers={"Accept": "application/json"}) assert response.status_code == 403 assert response.json["meta"]["code"] == 403 def test_default_unauthz_myjson(app, client): """Make sure render_json gets called for unauthn errors""" @app.security.render_json def my_json(payload, code, headers=None, user=None): return jsonify(dict(myresponse=payload, code=code)), code, headers authenticate(client, "joe@lp.com", "password") response = client.get("/admin", headers={"Accept": "application/json"}) assert response.status_code == 403 assert response.json["code"] == 403 def test_my_unauthz_handler(app, client): @app.security.unauthz_handler def my_unauthz(func_name, params): return ( jsonify(dict(myresponse={"func": func_name, "params": params}, code=403)), 403, ) authenticate(client, "joe@lp.com", "password") response = client.get("/admin", headers={"Accept": "application/json"}) assert response.status_code == 403 assert response.json["code"] == 403 assert response.json["myresponse"]["func"] == "roles_required" assert response.json["myresponse"]["params"] == ["admin"] def test_my_unauthz_handler_exc(app, client): """Verify that can use exceptions in unauthz handler""" @app.security.unauthz_handler def my_unauthz(func, params): raise ValueError("Bad Value") @app.errorhandler(ValueError) def error_handler(ex): return jsonify(dict(code=403)), 403 authenticate(client, "joe@lp.com", "password") response = client.get("/admin", headers={"Accept": "application/json"}) assert response.status_code == 403 flask-security-5.7.1/tests/test_templates.py000066400000000000000000000170241511046741400212510ustar00rootroot00000000000000""" test_templates ~~~~~~~~~~ Test templates to be W3C valid :copyright: (c) 2025-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. Note that the validator definitely rate-limits us - so we don't just run them all. To run a test pass templates="RESET,LOGIN" on the command line. """ import re from time import sleep import pytest import requests from flask import g from tests.test_utils import ( authenticate, logout, capture_reset_password_requests, get_form_input_value, reset_fresh, setup_tf_sms, ) from tests.test_webauthn import HackWebauthnUtil, reg_first_key def check_message(msgs, mtype="error"): # list of JSON messages from validator errors = [] for msg in msgs: if msg["type"] == mtype: errors.append(msg["message"]) return errors def check_template(name, client, r): # returns list of validation errors t = client.get(name) if t.status_code != 200: return [f"{name} error {t.status_code}"] return check_template_rdata(name, r, t.data) def check_template_rdata(name, r, rdata): vout = r.post("https://validator.w3.org/nu/?out=json", rdata) if vout.status_code == 429: # we got rate limited try again sleep(2.0) vout = r.post("https://validator.w3.org/nu/?out=json", rdata) if vout.status_code != 200: return [f"{name} API error {vout.status_code}"] if vout.status_code != 200: return [f"{name} API error {vout.status_code}"] return check_message(vout.json()["messages"]) def get_script_tags(html): """Return list of script tags in the HTML""" return re.findall(r"]*>", html, re.DOTALL | re.IGNORECASE) @pytest.mark.registerable() @pytest.mark.recoverable() @pytest.mark.changeable() @pytest.mark.change_email() @pytest.mark.change_username() @pytest.mark.oauth() @pytest.mark.username_recovery() @pytest.mark.unified_signin() @pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) @pytest.mark.two_factor() @pytest.mark.settings( multi_factor_recovery_codes=True, oauth_enable=True, ) @pytest.mark.csrf() def test_valid_html(app, client): # since we get rate limited - use external pytest option to specify totry = app.config.get("TEMPLATES", "").split(",") rsession = requests.session() rsession.headers.update({"Content-Type": "text/html; charset=utf-8"}) unauth_urls = [ "LOGIN", "REGISTER", "RESET", "US_SIGNIN", "USERNAME_RECOVERY", "WAN_SIGNIN", ] auth_urls = [ "CHANGE", "CHANGE_USERNAME", "CHANGE_EMAIL", "MULTI_FACTOR_RECOVERY_CODES", "TWO_FACTOR_SETUP", "US_SETUP", "US_VERIFY", "WAN_REGISTER", "WAN_VERIFY", ] # MULTI_FACTOR_RECOVERY requires tf-setup and login/password # TWO_FACTOR_RESCUE has an issue with the RadioField # TWO_FACTOR_SELECT needs special setup authenticate(client, csrf=True) response = client.get("/change") csrf_token = get_form_input_value(response, "csrf_token") reg_first_key(client, csrf_token=csrf_token) logout(client) terrors = dict() for t in [u for u in unauth_urls if u in totry]: terrors[t] = check_template(app.config[f"SECURITY_{t}_URL"], client, rsession) authenticate(client, csrf=True) for t in [u for u in auth_urls if u in totry]: if t == "US_SETUP": response = client.get("/us-setup") csrf_token = get_form_input_value(response, "csrf_token") response = client.post( "us-setup", data=dict(chosen_method="authenticator", csrf_token=csrf_token), ) terrors[t] = check_template_rdata("US_SETUP", rsession, response.data) continue if t == "TWO_FACTOR_SETUP": response = client.get("/tf-setup") csrf_token = get_form_input_value(response, "csrf_token") response = client.post( "tf-setup", data=dict(setup="authenticator", csrf_token=csrf_token), ) terrors[t] = check_template_rdata( "TWO_FACTOR_SETUP", rsession, response.data ) continue elif t == "US_VERIFY" or t == "VERIFY": reset_fresh(client, app.config["SECURITY_FRESHNESS"]) terrors[t] = check_template(app.config[f"SECURITY_{t}_URL"], client, rsession) print(f"Validated: {totry}") errors = {k: v for k, v in terrors.items() if v} assert not any(errors), errors @pytest.mark.confirmable() @pytest.mark.csrf() def test_valid_html_confirm(app, client): rsession = requests.session() rsession.headers.update({"Content-Type": "text/html; charset=utf-8"}) # since we get rate limited - use external pytest option to specify totry = app.config.get("TEMPLATES", "").split(",") if "CONFIRM" in totry: print(f"Validated: {totry}") terrors = check_template(app.config["SECURITY_CONFIRM_URL"], client, rsession) assert not terrors @pytest.mark.recoverable() @pytest.mark.csrf() def test_valid_html_recover(app, client): rsession = requests.session() rsession.headers.update({"Content-Type": "text/html; charset=utf-8"}) # since we get rate limited - use external pytest option to specify totry = app.config.get("TEMPLATES", "").split(",") if "RESET" in totry: print(f"Validated: {totry}") with capture_reset_password_requests() as resets: response = client.get("/reset") csrf_token = get_form_input_value(response, "csrf_token") client.post("/reset", data=dict(email="joe@lp.com", csrf_token=csrf_token)) token = resets[0]["token"] terrors = check_template( f'{app.config[f"SECURITY_RESET_URL"]}/{token}', client, rsession ) assert not terrors @pytest.mark.two_factor() @pytest.mark.csrf() def test_valid_html_rescue(app, client): rsession = requests.session() rsession.headers.update({"Content-Type": "text/html; charset=utf-8"}) # since we get rate limited - use external pytest option to specify totry = app.config.get("TEMPLATES", "").split(",") if "TWO_FACTOR_RESCUE" in totry: authenticate(client, csrf=True) response = client.get("/tf-setup") csrf_token = get_form_input_value(response, "csrf_token") setup_tf_sms(client, csrf_token=csrf_token) logout(client) authenticate(client, csrf=True) print(f"Validated: {totry}") terrors = check_template( app.config["SECURITY_TWO_FACTOR_RESCUE_URL"], client, rsession ) assert not terrors @pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) def test_script_nonce(app, client): """Test that script nonces appear in script tags when configured.""" @app.before_request def set_nonce(): g.csp_nonce = "12345" # default config -> no nonces in script tags res = client.get("/wan-signin") tags = get_script_tags(res.text) assert tags assert all("nonce" not in t for t in tags) # invalid nonce key given -> no nonces in script tags app.config["SECURITY_SCRIPT_NONCE_KEY"] = "wrong_key" res = client.get("/wan-signin") tags = get_script_tags(res.text) assert tags assert all("nonce" not in t for t in tags) # nonces set up correctly -> nonce in script tags app.config["SECURITY_SCRIPT_NONCE_KEY"] = "csp_nonce" res = client.get("/wan-signin") tags = get_script_tags(res.text) assert tags assert all('nonce="12345"' in t for t in tags) flask-security-5.7.1/tests/test_tf_plugin.py000066400000000000000000000106571511046741400212470ustar00rootroot00000000000000""" test_tf_plugin ~~~~~~~~~~~~~~~~~ tf_plugin tests :copyright: (c) 2022-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import json import pytest from tests.test_utils import ( get_form_action, get_session, get_existing_session, logout, setup_tf_sms, ) from tests.test_two_factor import tf_in_session from tests.test_webauthn import HackWebauthnUtil, wan_signin, reg_2_keys pytest.importorskip("webauthn") @pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) @pytest.mark.two_factor() def test_tf_select(app, client, get_message): # Test basic select mechanism when more than one 2FA has been setup wankeys = reg_2_keys(client) # add a webauthn 2FA key (authenticates) sms_sender = setup_tf_sms(client) logout(client) # since we have 2 2FA methods configured - we should get the tf-select form # also - test that we correctly propagate 'next' all the way through response = client.post( "/login?next=/profile", data=dict(email="matt@lp.com", password="password"), follow_redirects=True, ) assert b"Select Two-Factor Method" in response.data tf_select_url = get_form_action(response) response = client.post( tf_select_url, data=dict(which="webauthn"), follow_redirects=True ) assert b"Use a Passkey as a Second Factor" in response.data wan_signin_url = get_form_action(response) assert "/wan-signin?next=/profile" == wan_signin_url response = wan_signin( client, "matt@lp.com", wankeys["secondary"]["signin"], wan_signin_url ) assert not tf_in_session(get_existing_session(client)) assert b"Profile Page" in response.data # now do other 2FA logout(client) response = client.post( "/login", data=dict(email="matt@lp.com", password="password"), follow_redirects=True, ) assert b"Select Two-Factor Method" in response.data response = client.post("/tf-select", data=dict(which="sms"), follow_redirects=True) assert b"Please enter your authentication code generated via: SMS" in response.data code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your code has been confirmed" in response.data assert not tf_in_session(get_session(response)) # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 assert not tf_in_session(get_existing_session(client)) @pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) @pytest.mark.two_factor() def test_tf_select_json(app, client, get_message): # Test basic select mechanism when more than one 2FA has been setup headers = {"Accept": "application/json", "Content-Type": "application/json"} wankeys = reg_2_keys(client) # add a webauthn 2FA key (authenticates) setup_tf_sms(client) logout(client) # since we have 2 2FA methods configured - we should get the tf-select form response = client.post( "/login", json=dict(email="matt@lp.com", password="password") ) assert response.json["response"]["tf_required"] choices = response.json["response"]["tf_setup_methods"] assert all(k in choices for k in ["sms", "webauthn"]) # should get same answer for GET on /tf-select response = client.get("/tf-select", headers=headers) choices = response.json["response"]["tf_setup_methods"] assert all(k in choices for k in ["sms", "webauthn"]) # use webauthn as the second factor response = client.post("/tf-select", json=dict(which="webauthn")) signin_url = response.json["response"]["tf_signin_url"] response = client.post(signin_url, headers=headers) response_url = f'wan-signin/{response.json["response"]["wan_state"]}' response = client.post( response_url, json=dict(credential=json.dumps(wankeys["secondary"]["signin"])), ) assert response.status_code == 200 assert not tf_in_session(get_existing_session(client)) response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.two_factor() @pytest.mark.settings(url_prefix="/api") def test_tf_select_auth(app, client, get_message): # /tf-select is an unauthenticated endpoint - make sure only allowable in correct # state. response = client.get("/api/tf-select", follow_redirects=False) assert "/api/login" in response.location flask-security-5.7.1/tests/test_trackable.py000066400000000000000000000063051511046741400212030ustar00rootroot00000000000000""" test_trackable ~~~~~~~~~~~~~~ Trackable tests """ import datetime import pytest from flask import after_this_request, redirect from werkzeug.middleware.proxy_fix import ProxyFix from flask_security import login_user from tests.test_utils import authenticate, logout pytestmark = pytest.mark.trackable() def _client_ip(client): """Compatibility layer for Flask<0.12.""" return getattr(client, "environ_base", {}).get("REMOTE_ADDR") def test_trackable_flag(app, client): app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) e = "matt@lp.com" authenticate(client, email=e) logout(client) authenticate(client, email=e, headers={"X-Forwarded-For": "127.0.0.1"}) with app.app_context(): user = app.security.datastore.find_user(email=e) assert user.last_login_at is not None assert user.current_login_at is not None assert user.last_login_ip == _client_ip(client) assert user.current_login_ip == "127.0.0.1" assert user.login_count == 2 def test_trackable_with_multiple_ips_in_headers(app, client): app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2) e = "matt@lp.com" authenticate(client, email=e) logout(client) authenticate( client, email=e, headers={"X-Forwarded-For": "99.99.99.99, 88.88.88.88, 77.77.77.77"}, ) with app.app_context(): user = app.security.datastore.find_user(email=e) assert user.last_login_at is not None assert user.current_login_at is not None assert user.last_login_ip == _client_ip(client) assert user.current_login_ip == "88.88.88.88" assert user.login_count == 2 def test_trackable_using_login_user(app, client): """ This tests is only to serve as an example of how one needs to call datastore.commit() after logging a user in to make sure the trackable fields are saved to the datastore. """ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) @app.route("/login_custom", methods=["POST"]) def login_custom(): user = app.security.datastore.find_user(email=e) login_user(user) @after_this_request def save_user(response): app.security.datastore.commit() return response return redirect("/") e = "matt@lp.com" authenticate(client, email=e) logout(client) data = dict(email=e, password="password", remember="y") client.post("/login_custom", data=data, headers={"X-Forwarded-For": "127.0.0.1"}) with app.app_context(): user = app.security.datastore.find_user(email=e) assert user.last_login_at is not None assert user.current_login_at is not None assert user.last_login_ip == _client_ip(client) assert user.current_login_ip == "127.0.0.1" assert user.login_count == 2 @pytest.mark.settings(datetime_factory=lambda: datetime.datetime(2024, 3, 24)) def test_trackable_datetime(app, client): app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) e = "matt@lp.com" authenticate(client, email=e) with app.app_context(): user = app.security.datastore.find_user(email=e) assert user.current_login_at == datetime.datetime(2024, 3, 24) assert user.login_count == 1 flask-security-5.7.1/tests/test_two_factor.py000066400000000000000000001751151511046741400214300ustar00rootroot00000000000000""" test_two_factor ~~~~~~~~~~~~~~~~~ two_factor tests :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from datetime import date, timedelta import re from freezegun import freeze_time import markupsafe from passlib.totp import TOTP import pytest from flask_principal import identity_changed from flask_security import ( SQLAlchemyUserDatastore, SmsSenderFactory, reset_password_instructions_sent, tf_profile_changed, uia_email_mapper, ) from tests.test_utils import ( SmsBadSender, SmsTestSender, authenticate, capture_flashes, capture_send_code_requests, check_location, check_xlation, get_form_action, get_form_input_value, get_session, is_authenticated, json_authenticate, logout, ) pytestmark = pytest.mark.two_factor() SmsSenderFactory.senders["test"] = SmsTestSender SmsSenderFactory.senders["bad"] = SmsBadSender 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", 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] if json: response = client.post( "/tf-validate", json=dict(code=code), ) assert response.status_code == 200 else: response = client.post( "/tf-validate", data=dict(code=code), follow_redirects=True ) assert response.status_code == 200 def tf_in_session(session): return any( k in session for k in [ "tf_state", "tf_primary_method", "tf_user_id", "tf_remember_login", "tf_totp_secret", "tf_select", ] ) @pytest.mark.settings(two_factor_always_validate=False) def test_always_validate(app, client, get_message): tf_authenticate(app, client, remember=True) assert client.get_cookie("tf_validity") 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.delete_cookie("tf_validity") # Test JSON (this authenticates gal@lp.com) tf_authenticate(app, client, json=True, remember=True) logout(client) data = dict(email="gal@lp.com", password="password") response = client.post( "/login", json=data, follow_redirects=True, ) assert response.status_code == 200 # verify logged in is_authenticated(client, get_message) logout(client) data["email"] = "gal2@lp.com" response = client.post( "/login", json=data, follow_redirects=True, ) 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 tf_authenticate(app, client, json=True) logout(client) assert not client.get_cookie("tf_validity") data = dict(email="gal@lp.com", password="password") 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 tf_authenticate(app, client, json=True, remember=True) logout(client) data = dict(email="gal@lp.com", password="password") 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.delete_cookie("tf_validity") # Test JSON 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") 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.delete_cookie("tf_validity") # Test JSON 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") 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): # trying to pick method without doing earlier stage data = dict(setup="email") with capture_flashes() as flashes: response = client.post("/tf-setup", data=data) assert response.status_code == 302 assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "TWO_FACTOR_PERMISSION_DENIED" ) @pytest.mark.settings(two_factor_required=True, url_prefix="/api") def test_two_factor_illegal_state(app, client, get_message): # trying to pick method without doing earlier stage data = dict(setup="email") with capture_flashes() as flashes: response = client.post("/api/tf-setup", data=data) assert response.status_code == 302 assert "/api/login" in response.location assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "TWO_FACTOR_PERMISSION_DENIED" ) # try validate code response = client.post( "/api/tf-validate", data=dict(code=b"333"), follow_redirects=False ) assert response.status_code == 302 assert "/api/login" in response.location # try rescue response = client.post( "/api/tf-rescue", data=dict(help_setup="lost_device"), follow_redirects=False ) assert response.status_code == 302 assert "/api/login" in response.location @pytest.mark.settings(two_factor_required=True) def test_two_factor_flag(app, clients, get_message, outbox): # trying to verify code without going through two-factor # first login function client = clients wrong_code = b"000000" response = client.post( "/tf-validate", data=dict(code=wrong_code), follow_redirects=True ) message = b"You currently do not have permissions to access this page" assert message in response.data # Test login using invalid email data = dict(email="nobody@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) assert b"Specified user does not exist" in response.data response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert b"Specified user does not exist" in response.data # Test login using valid email and invalid password data = dict(email="gal@lp.com", password="wrong_pass") response = client.post("/login", data=data, follow_redirects=True) assert b"Invalid password" in response.data response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert b"Invalid password" in response.data # Test two-factor authentication first login data = dict(email="matt@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) message = b"Two-Factor authentication adds an extra layer of security" assert message in response.data response = client.post( "/tf-setup", data=dict(setup="not_a_method"), follow_redirects=True ) assert b"Marked method is not valid" in response.data with client.session_transaction() as session: assert session["tf_state"] == "setup_from_login" # try non-existing setup on setup page (using json) data = dict(setup="not_a_method") response = client.post( "/tf-setup", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert response.status_code == 400 assert ( response.json["response"]["field_errors"]["setup"][0] == "Marked method is not valid" ) data = dict(setup="email") response = client.post( "/tf-setup", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) # Test for sms in process of valid login sms_sender = SmsSenderFactory.createSender("test") data = dict(email="gal@lp.com", password="password") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert b'"code": 200' in response.data assert sms_sender.get_count() == 1 session = get_session(response) assert session["tf_state"] == "ready" code = sms_sender.messages[0].split()[-1] # submit bad token to two_factor_token_validation response = client.post("/tf-validate", data=dict(code=wrong_code)) assert get_message("TWO_FACTOR_INVALID_TOKEN") in response.data # submit right token and show appropriate response response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data # Upon completion, session cookie shouldn't have any two factor stuff in it. assert not tf_in_session(get_session(response)) # Test change two_factor view from sms to mail setup_data = dict(setup="email") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) msg = b"Enter code to complete setup" assert msg in response.data # Fetch token validate form response = client.get("/tf-validate") assert response.status_code == 200 # make sure two_factor_verify_code_form is set assert b'name="code"' in response.data code = outbox[1].body.split()[-1] # submit 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 authenticator # Setup authenticator setup_data = dict(setup="authenticator") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) 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 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 logout(client) # Test login with remember_token assert not client.get_cookie("remember_token") data = dict(email="gal@lp.com", password="password", remember=True) response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) # Generate token from passed totp_secret code = totp.generate().token response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data # Verify that the remember token is properly set assert client.get_cookie("remember_token") response = logout(client) # Verify that logout clears session info assert not tf_in_session(get_session(response)) # Test two-factor authentication first login data = dict(email="matt@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) message = b"Two-Factor authentication adds an extra layer of security" assert message in response.data # 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 an authenticator app on your device" in response.data assert b"data:image/svg+xml;base64," in response.data # check appearance of setup page when sms picked and phone number entered sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Enter code to complete setup" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data assert not tf_in_session(get_session(response)) logout(client) # check when two_factor_rescue function should not appear rescue_data_json = dict(help_setup="lost_device") response = client.post( "/tf-rescue", json=rescue_data_json, headers={"Content-Type": "application/json"}, ) assert b'"code": 400' in response.data # check when two_factor_rescue function should appear 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 rescue_data = dict(help_setup="email") response = client.post("/tf-rescue", data=rescue_data, follow_redirects=True) message = b"The code for authentication was sent to your email address" assert message in response.data rescue_data = dict(help_setup="help") response = client.post("/tf-rescue", data=rescue_data, follow_redirects=True) message = b"An email was sent to us in order to reset your application account" assert message in response.data @pytest.mark.settings(two_factor_rescue_email=False) def test_no_rescue_email(app, client): headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.post( "/login", json=dict(email="gal2@lp.com", password="password") ) assert response.json["response"]["tf_required"] response = client.get("/tf-rescue", headers=headers) options = response.json["response"]["recovery_options"] assert len(options.keys()) == 1 assert "help" in options.keys() # make sure that even if post using email - we get an error response = client.post("/tf-rescue", json=dict(help_setup="email")) assert response.status_code == 400 assert ( response.json["response"]["field_errors"]["help_setup"][0] == "Not a valid choice." ) @pytest.mark.settings(two_factor_required=True) def test_setup_bad_phone(app, client, get_message): data = dict(email="matt@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) message = b"Two-Factor authentication adds an extra layer of security" assert message in response.data sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="555-1212") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Phone number not valid" in response.data assert sms_sender.get_count() == 0 # require phone number with SMS data = dict(setup="sms") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Phone number not valid" in response.data assert sms_sender.get_count() == 0 # 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 get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") 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 login/setup using JSON. """ 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=headers) 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" # 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)) 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) 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) 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) assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" # send bad code response = client.post("/tf-validate", json=dict(code="whatsup")) assert response.status_code == 400 assert response.json["response"]["field_errors"]["code"][0] == "Invalid code" assert response.json["response"]["errors"][0] == "Invalid code" # Do a GET - should get recovery options response = client.get("/tf-validate", headers=headers) options = response.json["response"]["recovery_options"] assert "email" in options.keys() assert "/tf-rescue" in options["email"] # now send correct code code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", json=dict(code=code)) 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, outbox): # it's an error if not primary authenticated rescue_data_json = dict(help_setup="help") response = client.post( "/tf-rescue", json=rescue_data_json, ) 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) assert response.json["response"]["tf_required"] rescue_data = dict(help_setup="email") response = client.post("/tf-rescue", json=rescue_data) 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))) assert response.status_code == 200 logout(client) # Try rescue with no email (should send email to admin) client.post("/login", json=data) rescue_data = dict(help_setup="help") response = client.post("/tf-rescue", json=rescue_data) assert response.status_code == 200 assert outbox[1].recipients == ["helpme@myapp.com"] assert outbox[1].sender == "no-reply@localhost" assert outbox[1].subject == "Two-Factor Rescue" assert "gal2@lp.com" in outbox[1].body @pytest.mark.settings(two_factor_required=True) def test_json_auth_token(app, client): """ Test getting auth_token with two-factor """ 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=headers) assert response.status_code == 200 assert response.json["response"]["tf_required"] # Verify SMS sent assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate?include_auth_token", json=dict(code=code)) assert response.status_code == 200 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 @pytest.mark.settings(two_factor_required=True) def test_no_opt_out(app, client, get_message): # Test if 2FA required, can't opt-out. sms_sender = SmsSenderFactory.createSender("test") response = client.post( "/login", data=dict(email="gal@lp.com", password="password"), follow_redirects=True, ) assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # submit right token and show appropriate response response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data response = client.get("/tf-setup", follow_redirects=True) assert b"Disable two factor" not in response.data assert b"Currently setup two-factor method: SMS" in response.data # Try to opt-out data = dict(setup="disable") response = client.post("/tf-setup", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Marked method is not valid" in response.data @pytest.mark.settings( two_factor_setup_url="/custom-setup", two_factor_rescue_url="/custom-rescue" ) def test_custom_urls(client): response = client.get("/tf-setup") assert response.status_code == 404 response = client.get("/custom-setup") assert response.status_code == 302 response = client.get("/custom-rescue") assert response.status_code == 302 def test_evil_validate(app, client): """ Test logged in, and randomly try to validate a token """ signalled_identity = [] @identity_changed.connect_via(app) def on_identity_changed(app, identity): signalled_identity.append(identity.id) response = authenticate(client, "jill@lp.com") session = get_session(response) assert "tf_state" not in session 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 response = client.post("/tf-validate", data=dict(code="?"), follow_redirects=True) # This should log us out since it thinks we are evil assert not signalled_identity[0] del signalled_identity[:] def test_opt_in(app, client, get_message): """ Test entire lifecycle of user not having 2FA - setting it up, then deciding to turn it back off All using forms based API """ signalled_identity = [] @identity_changed.connect_via(app) def on_identity_changed(app, identity): signalled_identity.append(identity.id) response = authenticate(client, "jill@lp.com") session = get_session(response) assert "tf_state" not in session 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 sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Enter code to complete setup" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # Validate token - this should complete 2FA setup response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed" in response.data assert check_location(app, response.history[0].location, "/tf-setup") # Upon completion, session cookie shouldnt have any two factor stuff in it. session = get_session(response) assert not tf_in_session(session) # Log out logout(client) assert not signalled_identity[0] del signalled_identity[:] # Login now should require 2FA with sms sms_sender = SmsSenderFactory.createSender("test") response = authenticate(client, "jill@lp.com") session = get_session(response) assert session["tf_state"] == "ready" assert len(signalled_identity) == 0 assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data # Verify now logged in 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. 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 # Log out logout(client) assert not signalled_identity[0] del signalled_identity[:] # Should be able to log in with just user/pass response = authenticate(client, "jill@lp.com") session = get_session(response) assert "tf_state" not in session with app.app_context(): user = app.security.datastore.find_user(email="jill@lp.com") assert signalled_identity[0] == user.fs_uniquifier def test_opt_in_nc(app, client_nc, get_message): """ Test tf-setup without cookies """ response = json_authenticate(client_nc, "jill@lp.com") assert response.status_code == 200 token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": token, "Accept": "application/json"} sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client_nc.post("/tf-setup", json=data, headers=headers) assert response.status_code == 200 state_token = response.json["response"]["tf_state_token"] assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # send bad token response = client_nc.post( "/tf-setup/not-a-token", json=dict(code=code), headers=headers ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # send bad code response = client_nc.post( f"/tf-setup/{state_token}", json=dict(code=12345), headers=headers ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "TWO_FACTOR_INVALID_TOKEN" ) # Validate token - this should complete 2FA setup @tf_profile_changed.connect_via(app) def pc(sender, user, method, **kwargs): assert method == "sms" assert user.tf_phone_number == "+442083661177" response = client_nc.post( f"/tf-setup/{state_token}", json=dict(code=code), headers=headers ) assert response.status_code == 200 response = client_nc.get("/tf-setup", headers=headers) assert response.json["response"]["tf_method"] == "sms" assert response.json["response"]["tf_phone_number"] == "+442083661177" def test_opt_in_nc_expired(app, client_nc, get_message): """ Test tf-setup without cookies - expired token """ with freeze_time( date.today() + timedelta(days=-1) ): # older than TWO_FACTOR_SETUP_WITHIN response = json_authenticate(client_nc, "jill@lp.com") assert response.status_code == 200 token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": token, "Accept": "application/json"} sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client_nc.post("/tf-setup", json=data, headers=headers) assert response.status_code == 200 state_token = response.json["response"]["tf_state_token"] assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # Validate token - this should complete 2FA setup response = client_nc.post( f"/tf-setup/{state_token}", json=dict(code=code), headers=headers ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "TWO_FACTOR_SETUP_EXPIRED", within=app.config["SECURITY_TWO_FACTOR_SETUP_WITHIN"], ) def test_opt_in_state_token(app, client, get_message): """ Test using forms and new state_token approach (rather than sessions to store intermediate state) """ authenticate(client, "jill@lp.com") # 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) assert b"Enter code to complete setup" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] verify_url = get_form_action(response, 1) # this will be with state_token # send in bad token response = client.post( "/tf-setup/not-a-token", data=dict(code=code), follow_redirects=True ) assert check_location(app, response.history[0].location, "/tf-setup") assert get_message("API_ERROR") in response.data # send in bad code response = client.post(verify_url, data=dict(code=12345), follow_redirects=True) assert check_location(app, response.history[0].location, "/tf-setup") assert get_message("TWO_FACTOR_INVALID_TOKEN") in response.data # Validate token - this should complete 2FA setup response = client.post(verify_url, data=dict(code=code), follow_redirects=True) assert b"You successfully changed" in response.data assert check_location(app, response.history[0].location, "/tf-setup") # Upon completion, session cookie shouldn't have any two factor stuff in it. session = get_session(response) assert not tf_in_session(session) response = client.get("/tf-setup") assert b"Disable two-factor" in response.data assert b"Currently setup two-factor method: SMS" in response.data def test_opt_out_json(app, client, get_message): 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 assert is_authenticated(client, get_message) response = client.get("tf-setup", headers=headers) assert "disable" not in response.json["response"]["tf_available_methods"] @pytest.mark.filterwarnings("ignore") @pytest.mark.recoverable() @pytest.mark.settings(two_factor_required=True, auto_login_after_reset=True) def test_recoverable(app, client, get_message): # make sure 'forgot password' doesn't bypass 2FA. # 'gal@lp.com' already setup for SMS rtokens = [] sms_sender = SmsSenderFactory.createSender("test") @reset_password_instructions_sent.connect_via(app) def on_instructions_sent(sapp, **kwargs): rtokens.append(kwargs["token"]) client.post("/reset", data=dict(email="gal@lp.com"), follow_redirects=True) response = client.post( "/reset/" + rtokens[0], data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, ) # Should have redirected us to the 2FA login page assert b"Please enter your authentication code" in response.data # we shouldn't be logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 302 # Grab code that was sent assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data # verify we are logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(two_factor_required=True) def test_admin_setup_reset(app, client, get_message): # Verify can use administrative datastore method to setup SMS # and that administrative reset removes access. sms_sender = SmsSenderFactory.createSender("test") data = dict(email="gene@lp.com", password="password") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) assert response.json["response"]["tf_required"] # we shouldn't be logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 302 assert response.location == "/login?next=/profile" # Use admin to setup gene's SMS/phone. with app.app_context(): user = app.security.datastore.find_user(email="gene@lp.com") totp_secret = app.security._totp_factory.generate_totp_secret() app.security.datastore.tf_set(user, "sms", totp_secret, phone="+442083661177") app.security.datastore.commit() response = authenticate(client, "gene@lp.com") session = get_session(response) assert session["tf_state"] == "ready" # Grab code that was sent assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data # verify we are logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # logout logout(client) # use administrative reset method with app.app_context(): user = app.security.datastore.find_user(email="gene@lp.com") app.security.datastore.reset_user_access(user) app.security.datastore.commit() data = dict(email="gene@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" # we shouldn't be logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 302 @pytest.mark.settings(two_factor_required=True) def test_datastore(app, clients, get_message): # Test that user record is properly set after proper 2FA setup. client = clients sms_sender = SmsSenderFactory.createSender("test") data = dict(email="gene@lp.com", password="password") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) assert response.json["meta"]["code"] == 200 session = get_session(response) assert session["tf_state"] == "setup_from_login" # setup data = dict(setup="sms", phone="+442083661177") response = client.post( "/tf-setup", json=data, headers={"Content-Type": "application/json"} ) assert sms_sender.get_count() == 1 session = get_session(response) assert session["tf_state"] == "validating_profile" assert session["tf_primary_method"] == "sms" code = sms_sender.messages[0].split()[-1] # submit token and show appropriate response response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data session = get_session(response) # Verify that successful login clears session info assert not tf_in_session(session) with app.app_context(): user = app.security.datastore.find_user(email="gene@lp.com") assert user.tf_primary_method == "sms" assert user.tf_phone_number == "+442083661177" assert "enckey" in user.tf_totp_secret def test_totp_secret_generation(app, client): """ Test the totp secret generation upon changing method to make sure it stays the same after the process is completed """ # Properly log in jill for this test signalled_identity = [] @identity_changed.connect_via(app) def on_identity_changed(app, identity): signalled_identity.append(identity.id) response = authenticate(client, "jill@lp.com") session = get_session(response) assert "tf_state" not in session with app.app_context(): user = app.security.datastore.find_user(email="jill@lp.com") assert signalled_identity[0] == user.fs_uniquifier assert not user.tf_totp_secret del signalled_identity[:] sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661188") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Enter code to complete setup" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # Retrieve the currently generated totp secret for later comparison session = get_session(response) generated_secret = session["tf_totp_secret"] assert "enckey" in generated_secret # Validate token - this should complete 2FA setup response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed" in response.data # Retrieve the final totp secret and make sure it matches the previous one with app.app_context(): user = app.security.datastore.find_user(email="jill@lp.com") assert generated_secret == user.tf_totp_secret # Finally opt back out and check that tf_totp_secret is None 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 with app.app_context(): user = app.security.datastore.find_user(email="jill@lp.com") assert user.tf_totp_secret is None # Log out logout(client) assert not signalled_identity[0] del signalled_identity[:] @pytest.mark.settings(two_factor_enabled_methods=["authenticator"]) def test_just_authenticator(app, client): authenticate(client, email="jill@lp.com") response = client.get("/tf-setup", follow_redirects=True) assert b"Set up using SMS" not in response.data data = dict(setup="authenticator") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Submit Code" in response.data # test json response = client.post("/tf-setup", json=data) assert response.status_code == 200 @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") setup_data = dict(setup="authenticator") response = client.post("/tf-setup", json=setup_data, headers=headers) assert response.json["response"]["tf_authr_issuer"] == "tests" 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": {"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") setup_data = dict(setup="authenticator") 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": {"mapper": uia_email_mapper}}, {"username": {"mapper": lambda x: x}}, ] ) def test_email_salutation(app, client, outbox): authenticate(client, email="jill@lp.com") response = client.post("/tf-setup", data=dict(setup="email"), follow_redirects=True) msg = b"Enter code to complete setup" assert msg in response.data assert "jill@lp.com" in outbox[0].recipients assert "jill@lp.com" in outbox[0].body assert "jill@lp.com" in outbox[0].alts["html"] @pytest.mark.settings( USER_IDENTITY_ATTRIBUTES=[ {"username": {"mapper": lambda x: "@" not in x}}, {"email": {"mapper": uia_email_mapper}}, ] ) def test_username_salutation(app, client, outbox): authenticate(client, email="jill@lp.com") response = client.post("/tf-setup", data=dict(setup="email"), follow_redirects=True) msg = b"Enter code to complete setup" assert msg in response.data assert "jill@lp.com" in outbox[0].recipients assert "jill@lp.com" not in outbox[0].body assert "jill@lp.com" not in outbox[0].alts["html"] assert "jill" in outbox[0].body @pytest.mark.settings(sms_service="bad") def test_bad_sender(app, client, get_message): # If SMS sender fails - make sure propagated # Test form, json, x signin, setup headers = {"Accept": "application/json", "Content-Type": "application/json"} # test normal, already setup up login. with capture_flashes() as flashes: data = {"email": "gal@lp.com", "password": "password"} response = client.post("login", data=data, follow_redirects=False) assert response.status_code == 302 assert "/login" in response.location assert get_message("FAILED_TO_SEND_CODE") in flashes[0]["message"].encode("utf-8") # test w/ JSON data = dict(email="gal@lp.com", password="password") response = client.post("login", json=data, headers=headers) assert response.status_code == 500 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "FAILED_TO_SEND_CODE" ) # Now test setup tf_authenticate(app, client) data = dict(setup="sms", phone="+442083661188") response = client.post("tf-setup", data=data) assert get_message("FAILED_TO_SEND_CODE") in response.data response = client.post("tf-setup", json=data, headers=headers) assert response.status_code == 500 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "FAILED_TO_SEND_CODE" ) def test_replace_send_code(app, get_message): pytest.importorskip("sqlalchemy") pytest.importorskip("flask_sqlalchemy") # replace tf_send_code - and have it return an error to check that. from flask_sqlalchemy import SQLAlchemy from flask_security.models import fsqla_v2 as fsqla from flask_security import Security, 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, fsqla.FsUserMixin): rv = [None, "That didnt work out as we planned", "Failed Again"] def tf_send_security_token(self, method, **kwargs): return User.rv.pop(0) 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"), tf_primary_method="sms", tf_totp_secret=app.security._totp_factory.generate_totp_secret(), ) ds.commit() data = dict(email="trp@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) assert b"Please enter your authentication code" in response.data rescue_data = dict(help_setup="email") response = client.post("/tf-rescue", data=rescue_data, follow_redirects=True) assert b"That didnt work out as we planned" in response.data # Test JSON headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.post("/tf-rescue", json=rescue_data, headers=headers) assert response.status_code == 500 assert response.json["response"]["field_errors"]["help_setup"][0] == "Failed Again" with app.app_context(): db.engine.dispose() def test_propagate_next(app, client): # verify we propagate the ?next param all the way through a two-factor login with capture_send_code_requests() as codes: data = dict(email="gal@lp.com", password="password") response = client.post("/login?next=/im-in", data=data, follow_redirects=True) assert "?next=/im-in" in response.request.url # grab URL from form to show that our template propagates ?next verify_url = get_form_action(response) response = client.post( verify_url, data=dict(code=codes[0]["login_token"]), follow_redirects=False ) assert "/im-in" in response.location logout(client) # do it with next in the form data = dict(email="gal@lp.com", password="password", next="/im-in") response = client.post("/login", data=data, follow_redirects=True) assert "?next=/im-in" in response.request.url # grab URL from form to show that our template propagates ?next verify_url = get_form_action(response) response = client.post( verify_url, data=dict(code=codes[1]["login_token"]), follow_redirects=False ) assert "/im-in" in response.location @pytest.mark.settings(freshness=timedelta(minutes=0)) def test_verify(app, client, get_message): # Test setup when reauthenticate required authenticate(client) response = client.get("tf-setup", follow_redirects=False) assert check_location(app, response.location, "/verify?next=/tf-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) assert get_message("REAUTHENTICATION_REQUIRED") in response.data verify_password_url = get_form_action(response) # 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 check_location(app, response.location, "/tf-setup") assert get_message("REAUTHENTICATION_SUCCESSFUL") == flashes[0]["message"].encode( "utf-8" ) def test_verify_json(app, client, get_message): # Test setup when reauthenticate 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"][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, outbox): pytest.importorskip("sqlalchemy") pytest.importorskip("flask_sqlalchemy") # 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(): ds.create_user( email="trp@lp.com", password=hash_password("password"), ) ds.commit() client = app.test_client() data = dict(email="trp@lp.com", password="password") client.post("/login", data=data, follow_redirects=True) response = client.post("/tf-setup", data=dict(setup="email"), follow_redirects=True) msg = b"Enter code to complete setup" assert msg in response.data code = outbox[0].body.split()[-1] # submit 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 with app.app_context(): db.engine.dispose() @pytest.mark.settings(two_factor_post_setup_view="/post_setup_view") def test_post_setup_redirect(app, client): authenticate(client, "jill@lp.com") sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Enter code to complete setup" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # Validate token - this should complete 2FA setup response = client.post("/tf-validate", data=dict(code=code), follow_redirects=False) assert check_location(app, response.location, "/post_setup_view") @pytest.mark.app_settings(babel_default_locale="fr_FR") @pytest.mark.babel() def test_xlation(app, client, get_message_local): # Test method translation assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" # login as gal2 which has 'authenticator' set up response = authenticate(client, email="gal2@lp.com", follow_redirects=True) with app.test_request_context(): existing = ( "Veuillez saisir votre code d'authentification généré via: authentificateur" ) assert markupsafe.escape(existing).encode() in response.data with app.app_context(): # generate 'code' as authenticator would and complete authentication user = app.security.datastore.find_user(email="gal2@lp.com") code = app.security._totp_factory.generate_totp_password(user.tf_totp_secret) client.post("/tf-validate", data=dict(code=code), follow_redirects=True) response = client.get("/tf-setup", follow_redirects=True) with app.test_request_context(): existing = "Méthode à deux facteurs actuellement configurée : authentificateur" assert markupsafe.escape(existing).encode() in response.data @pytest.mark.csrf(ignore_unauth=True) @pytest.mark.settings(two_factor_post_setup_view="/post_setup_view") def test_setup_csrf(app, client): # Verify /tf-setup properly handles CSRF and template relays CSRF errors tf_authenticate(app, client) response = client.get("tf-setup") assert b"Disable" in response.data csrf_token = get_form_input_value(response, "csrf_token") response = client.post("tf-setup", data=dict(setup="disable")) assert b"The CSRF token is missing" in response.data response = client.post( "tf-setup", data=dict(setup="disable", csrf_token=csrf_token) ) assert check_location(app, response.location, "/post_setup_view") @pytest.mark.csrf(ignore_unauth=True, csrfprotect=True) def test_setup_csrf_header(app, client): # Test that can setup using csrf token in header tf_authenticate(app, client) response = client.get("tf-setup", json=dict()) csrf_token = response.json["response"]["csrf_token"] response = client.post("tf-setup", json=dict(setup="disable")) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." response = client.post( "tf-setup", json=dict(setup="disable"), headers={"X-CSRF-Token": csrf_token} ) assert response.status_code == 200 @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(CSRF_COOKIE_NAME="XSRF-Token") def test_csrf_2fa_login_cookie(app, client): # Use XSRF-Token cookie for entire login sequence sms_sender = SmsSenderFactory.createSender("test") response = client.get( "/login", data={}, headers={"Content-Type": "application/json"} ) assert client.get_cookie("XSRF-Token") csrf_token = response.json["response"]["csrf_token"] assert csrf_token == client.get_cookie("XSRF-Token").value response = client.post( "/login", json=dict(email="gal@lp.com", password="password"), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert b'"code": 200' in response.data session = get_session(response) assert session["tf_state"] == "ready" assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post( "/tf-validate", json=dict(code=code), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert response.status_code == 200 # verify original session csrf_token still works. response = client.post( "/json_auth", json=dict(label="label"), headers={"Content-Type": "application/json", "X-CSRF-Token": csrf_token}, ) assert response.status_code == 200 # use XSRF_Cookie to send in csrf_token response = client.post( "/json_auth", json=dict(label="label"), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert response.status_code == 200 assert response.json["label"] == "label" @pytest.mark.csrf(ignore_unauth=True, csrfprotect=True) @pytest.mark.settings(CSRF_COOKIE_NAME="XSRF-Token") def test_csrf_2fa_nounauth_cookie(app, client): # use CSRF cookie when ignoring unauth endpoints sms_sender = SmsSenderFactory.createSender("test") response = client.post( "/login", json=dict(email="gal@lp.com", password="password"), headers={"Content-Type": "application/json"}, ) code = sms_sender.messages[0].split()[-1] response = client.post( "/tf-validate", json=dict(code=code), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 response = client.post( "/json_auth", json=dict(label="label"), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert response.status_code == 200 assert response.json["label"] == "label" flask-security-5.7.1/tests/test_unified_signin.py000066400000000000000000002464021511046741400222510ustar00rootroot00000000000000""" test_unified_signin ~~~~~~~~~~~~~~~~~~~ Unified signin tests :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import base64 from contextlib import contextmanager from datetime import timedelta import markupsafe from passlib.totp import TOTP import re from urllib.parse import parse_qsl, urlsplit import pytest from flask import Flask from tests.conftest import v2_param from tests.test_utils import ( SmsBadSender, SmsTestSender, FakeSerializer, authenticate, capture_flashes, capture_reset_password_requests, check_location, check_xlation, get_form_action, get_session, is_authenticated, logout, reset_fresh, reset_fresh_auth_token, setup_tf_sms, ) from tests.test_webauthn import HackWebauthnUtil, reg_2_keys 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 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(): login_requests = [] def _on(app, **data): assert isinstance(app, Flask) assert all(v in data for v in ["user", "method", "token"]) assert isinstance(data["user"], UserMixin) login_requests.append(data) us_security_token_sent.connect(_on) try: yield login_requests finally: us_security_token_sent.disconnect(_on) def us_authenticate(client, identity="matt@lp.com"): with capture_send_code_requests() as requests: response = client.post( "/us-signin/send-code", json=dict(identity=identity, chosen_method="email"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 response = client.post( "/us-signin?include_auth_token", json=dict(identity=identity, passcode=requests[0]["token"]), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 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 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("/"): user = app.security.datastore.find_user(email=email) totp_secret = app.security._totp_factory.generate_totp_secret() app.security.datastore.us_set(user, "sms", totp_secret, phone) app.security.datastore.commit() def set_email(app, email="matt@lp.com"): # A quick way to 'setup' email with app.test_request_context("/"): user = app.security.datastore.find_user(email=email) totp_secret = app.security._totp_factory.generate_totp_secret() app.security.datastore.us_set(user, "email", totp_secret) app.security.datastore.commit() def test_simple_signin(app, clients, get_message, outbox): set_email(app) auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) # Test missing choice data = dict(identity="matt@lp.com") 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 = clients.post("/us-signin/send-code", data=data, follow_redirects=True) assert get_message("US_SPECIFY_IDENTITY") in response.data # test disabled account set_email(app, email="gal3@lp.com") with app.test_request_context("/"): user = app.security.datastore.find_user(email="gal3@lp.com") app.security.datastore.deactivate_user(user) app.security.datastore.commit() data = dict(identity="gal3@lp.com", chosen_method="email") 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: response = clients.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 assert b"Sign In" in response.data assert len(requests) == 1 assert len(outbox) == 1 # try bad code response = clients.post( "/us-signin", data=dict(identity="matt@lp.com", passcode="blahblah"), follow_redirects=True, ) assert get_message("INVALID_PASSWORD_CODE") in response.data # Correct code assert not clients.get_cookie("remember_token") assert not clients.get_cookie("session") response = clients.post( "/us-signin", data=dict(identity="matt@lp.com", passcode=requests[0]["token"]), follow_redirects=False, ) assert not clients.get_cookie("remember_token") assert "email" in auths[0][1] assert is_authenticated(clients, get_message) logout(clients) assert not is_authenticated(clients, get_message) # login via SMS sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = clients.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="sms"), follow_redirects=True, ) assert response.status_code == 200 assert b"Sign In" in response.data code = sms_sender.messages[0].split()[-1].strip(".") response = clients.post( "/us-signin", data=dict(identity="matt@lp.com", passcode=code, remember=True), follow_redirects=True, ) assert response.status_code == 200 assert clients.get_cookie("remember_token") assert "sms" in auths[1][1] assert is_authenticated(clients, get_message) logout(clients) assert not clients.get_cookie("remember_token") def test_simple_signin_json(app, client_nc, get_message, outbox): set_email(app) auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) headers = {"Accept": "application/json", "Content-Type": "application/json"} with capture_flashes() as flashes: response = client_nc.get("/us-signin", headers=headers) jresponse = response.json["response"] assert ( jresponse["available_methods"] == app.config["SECURITY_US_ENABLED_METHODS"] ) assert jresponse["identity_attributes"] == get_identity_attributes(app=app) assert set(jresponse["code_methods"]) == {"email", "sms"} with capture_send_code_requests() as requests: response = client_nc.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="email"), headers=headers, follow_redirects=True, ) assert response.status_code == 200 assert "csrf_token" in response.json["response"] assert "user" not in response.json["response"] assert len(requests) == 1 assert len(outbox) == 1 # try bad code response = client_nc.post( "/us-signin", json=dict(identity="matt@lp.com", passcode="blahblah"), headers=headers, follow_redirects=True, ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_PASSWORD_CODE" ) # Login successfully with code response = client_nc.post( "/us-signin?include_auth_token", json=dict(identity="matt@lp.com", passcode=requests[0]["token"]), headers=headers, follow_redirects=True, ) assert response.status_code == 200 assert "authentication_token" in response.json["response"]["user"] assert "email" in auths[0][1] logout(client_nc) assert not is_authenticated(client_nc, get_message) # login via SMS sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = client_nc.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="sms"), headers=headers, follow_redirects=True, ) assert response.status_code == 200 code = sms_sender.messages[0].split()[-1].strip(".") response = client_nc.post( "/us-signin?include_auth_token", json=dict(identity="matt@lp.com", passcode=code), headers=headers, follow_redirects=True, ) assert response.status_code == 200 assert "authentication_token" in response.json["response"]["user"] 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 @pytest.mark.registerable() @pytest.mark.settings(password_required=False) def test_us_passwordless(app, client, get_message, outbox): # Check passwordless. # Check contents of email template - this uses a test template # in order to check all context vars since the default template # doesn't have all of them. response = client.post( "/register", data=dict(email="nopasswd-dude@lp.com"), follow_redirects=True ) logout(client) with capture_send_code_requests() as requests: response = client.post( "/us-signin/send-code", data=dict(identity="nopasswd-dude@lp.com", chosen_method="email"), follow_redirects=True, ) # 2 emails - first from registration. assert len(outbox) == 2 matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) # should be 5 - link, email, token, config item, username assert matcher[1].split(":")[1] == "nopasswd-dude@lp.com" token = matcher[2].split(":")[1] assert token == requests[0]["token"] # deprecated assert token == requests[0]["login_token"] assert matcher[3].split(":")[1] == "True" # register_blueprint assert matcher[4].split(":")[1] == "nopasswd-dude@lp.com" # check link link = matcher[0].split(":", 1)[1] response = client.get(link, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") in response.data # check us-setup has 'email' but not password response = client.get("/us-setup", json={}) assert response.json["response"]["active_methods"] == ["email"] @pytest.mark.registerable() @pytest.mark.confirmable() @pytest.mark.settings(password_required=False) def test_us_passwordless_confirm(app, client, get_message, outbox): # Check passwordless with confirmation required. response = client.post( "/register", data=dict(email="nopasswd-dude@lp.com"), follow_redirects=True ) # Try logging in - should get confirmation required. response = client.post( "/us-signin/send-code", data=dict(identity="nopasswd-dude@lp.com", chosen_method="email"), follow_redirects=True, ) assert get_message("CONFIRMATION_REQUIRED") in response.data # grab welcome email which has confirmation link (test version of welcome.txt) matcher = re.findall(r"\w+:.*", outbox[0].body, re.IGNORECASE) link = matcher[0].split(":", 1)[1] response = client.get(link, follow_redirects=True) assert get_message("EMAIL_CONFIRMED") in response.data logout(client) # should be able to authenticate now. response = client.post( "/us-signin/send-code", data=dict(identity="nopasswd-dude@lp.com", chosen_method="email"), follow_redirects=True, ) # 2 emails - first from registration. assert len(outbox) == 2 matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) # authenticate with link link = matcher[0].split(":", 1)[1] response = client.get(link, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") in response.data @pytest.mark.registerable() @pytest.mark.confirmable() @pytest.mark.settings(password_required=False) def test_us_passwordless_confirm_json(app, client, get_message, outbox): # Check passwordless with confirmation required. headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.post("/register", json=dict(email="nopasswd-dude@lp.com")) # Try logging in - should get confirmation required. response = client.post( "/us-signin/send-code", json=dict(identity="nopasswd-dude@lp.com", chosen_method="email"), ) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "CONFIRMATION_REQUIRED" ) # grab welcome email which has confirmation link (test version of welcome.txt) matcher = re.findall(r"\w+:.*", outbox[0].body, re.IGNORECASE) link = matcher[0].split(":", 1)[1] response = client.get(link, headers=headers, follow_redirects=False) assert check_location(app, response.location, "/login") # should be able to authenticate now. response = client.post( "/us-signin/send-code", json=dict(identity="nopasswd-dude@lp.com", chosen_method="email"), ) # 2 emails - first from registration. assert len(outbox) == 2 matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) # authenticate with link link = matcher[0].split(":", 1)[1] response = client.get(link, headers=headers, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") in response.data 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. headers = {"Accept": "application/json", "Content-Type": "application/json"} sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = client_nc.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="sms"), headers=headers, follow_redirects=True, ) assert response.status_code == 200 assert len(sms_sender.messages) == 1 code = sms_sender.messages[0].split()[-1].strip(".") response = client_nc.post( "/us-signin?include_auth_token", json=dict(identity="matt@lp.com", passcode=code), headers=headers, follow_redirects=True, ) assert response.status_code == 200 # logout, reset access logout(client_nc) 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() response = client_nc.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="sms"), headers=headers, follow_redirects=True, ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_METHOD_NOT_AVAILABLE" ) # Nothing should have been sent. assert len(sms_sender.messages) == 1 def test_admin_setup_reset(app, client_nc, get_message): # Test that we can setup SMS using datastore admin method, and that # the datastore admin reset (us_reset) disables it. headers = {"Accept": "application/json", "Content-Type": "application/json"} sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = client_nc.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="sms"), headers=headers, follow_redirects=True, ) assert response.status_code == 200 assert len(sms_sender.messages) == 1 with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.us_reset(user) app.security.datastore.commit() response = client_nc.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="sms"), headers=headers, follow_redirects=True, ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_METHOD_NOT_AVAILABLE" ) # Nothing should have been sent. assert len(sms_sender.messages) == 1 @pytest.mark.settings(post_login_view="/post_login") def test_get_already_authenticated(app, client): response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data # This should ignore next response = client.get("/us-signin?next=/page1", follow_redirects=True) assert b"Post Login" in response.data # should still get extra goodies headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("/us-signin", headers=headers) assert response.status_code == 200 jresponse = response.json["response"] assert all( a in jresponse for a in ["code_methods", "identity_attributes", "available_methods"] ) assert "authentication_token" not in jresponse["user"] assert all(a in jresponse["user"] for a in ["email", "last_update"]) @pytest.mark.settings(post_login_view="/post_login") def test_post_already_authenticated(client, get_message): response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data data = dict(email="matt@lp.com", password="password") response = client.post("/us-signin", data=data, follow_redirects=True) assert b"Post Login" in response.data headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.post("/us-signin", json=data, headers=headers) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "ANONYMOUS_USER_REQUIRED" ) @pytest.mark.settings(us_email_subject="Code For You") def test_verify_link(app, client, get_message, outbox): set_email(app) auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) with capture_send_code_requests() as requests: response = client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) 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 ) magic_link = matcher.group(1) # Try with missing code response = client.get("us-verify-link?id=matt@lp.com", follow_redirects=False) assert "/us-signin" in response.location response = client.get("us-verify-link?id=matt@lp.com", follow_redirects=True) assert get_message("API_ERROR") in response.data # Try unknown user response = client.get("us-verify-link?id=123435&code=12345", follow_redirects=True) assert get_message("USER_DOES_NOT_EXIST") in response.data # Try bad code response = client.get( f"us-verify-link?id={requests[0]['user'].fs_uniquifier}&code=12345", follow_redirects=True, ) assert get_message("INVALID_CODE") in response.data # Try actual link response = client.get(magic_link, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") in response.data assert "email" in auths[0][1] # verify logged in assert is_authenticated(client, get_message) @pytest.mark.settings( redirect_host="localhost:8081", redirect_behavior="spa", login_error_view="/login-error", post_login_view="/post-login", ) def test_verify_link_spa(app, client, get_message, outbox): # N.B. we use client here since this only works/ is supported if using # sessions. set_email(app) headers = {"Accept": "application/json", "Content-Type": "application/json"} with capture_send_code_requests() as requests: response = client.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="email"), headers=headers, ) assert response.status_code == 200 matcher = re.match( r".*(http://[^\s*]*).*", outbox[0].body, re.IGNORECASE | re.DOTALL ) magic_link = matcher.group(1) # Try with no code response = client.get("us-verify-link?id=matt@lp.com", follow_redirects=False) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) assert get_message("API_ERROR") == qparams["error"].encode("utf-8") # Try unknown user response = client.get("us-verify-link?id=98765&code=12345", follow_redirects=False) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) assert get_message("USER_DOES_NOT_EXIST") == qparams["error"].encode("utf-8") # Try bad code response = client.get( f"us-verify-link?id={requests[0]['user'].fs_uniquifier}&code=12345", follow_redirects=False, ) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) assert get_message("INVALID_CODE") == qparams["error"].encode("utf-8") # Try actual link response = client.get(magic_link, follow_redirects=False) assert response.status_code == 302 split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/post-login" == split.path qparams = dict(parse_qsl(split.query)) assert qparams["email"] == "matt@lp.com" assert is_authenticated(client, get_message) def test_setup(app, clients, get_message): tcl = clients set_email(app) us_authenticate(tcl) response = tcl.get("us-setup") # Email should be in delete options since we just set that up. assert all( i in response.data for i in [b"delete_method-0", b"chosen_method-0", b"chosen_method-1"] ) # test not supplying anything to do response = tcl.post("us-setup", data=dict(phone="6505551212")) assert get_message("API_ERROR") in response.data # test missing phone response = tcl.post("us-setup", data=dict(chosen_method="sms", phone="")) assert response.status_code == 200 assert get_message("PHONE_INVALID") in response.data # test invalid phone response = tcl.post( "us-setup", data=dict(chosen_method="sms", phone="NOT-A-NUMBER") ) assert response.status_code == 200 assert get_message("PHONE_INVALID") in response.data assert b"Enter code here to complete setup" not in response.data sms_sender = SmsSenderFactory.createSender("test") response = tcl.post( "us-setup", data=dict(chosen_method="sms", phone="650-555-1212") ) assert response.status_code == 200 assert b"Submit Code" in response.data assert b"Enter code here to complete setup" in response.data verify_url = get_form_action(response, 1) # Try invalid code response = tcl.post(verify_url, data=dict(passcode=12345), follow_redirects=True) assert get_message("INVALID_PASSWORD_CODE") in response.data code = sms_sender.messages[0].split()[-1].strip(".") response = tcl.post(verify_url, data=dict(passcode=code), follow_redirects=True) assert response.status_code == 200 assert get_message("US_SETUP_SUCCESSFUL") in response.data def test_setup_email(app, client, get_message, outbox): # setup with email - make sure magic link isn't sent and code is. # N.B. this is using the test us_instructions template set_email(app) us_authenticate(client) response = client.post("us-setup", data=dict(chosen_method="email")) assert response.status_code == 200 assert b"Enter code here to complete setup" in response.data verify_url = get_form_action(response, 1) # verify no magic link - us_authenticate received first email - we want the second matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) # should be 4 - link, email, token, config item assert matcher[0].split(":")[1] == "None" assert matcher[1].split(":")[1] == "matt@lp.com" code = matcher[2].split(":")[1] response = client.post(verify_url, data=dict(passcode=code), follow_redirects=True) assert response.status_code == 200 assert get_message("US_SETUP_SUCCESSFUL") in response.data @pytest.mark.settings( us_enabled_methods=["email", "sms"], user_identity_attributes=UIA_EMAIL_PHONE, freshness=timedelta(hours=-1), ) def test_setup_json(app, client_nc, get_message): # This shows that by setting freshness to negative doesn't require session. @us_profile_changed.connect_via(app) def pc(sender, user, methods, delete, **kwargs): assert not delete assert methods == ["sms"] assert user.us_phone_number == "+16505551212" set_email(app) token = us_authenticate(client_nc) headers = { "Authentication-Token": token, "Accept": "application/json", "Content-Type": "application/json", } response = client_nc.get("/us-setup", headers=headers) assert response.status_code == 200 assert response.json["response"]["available_methods"] == ["email", "sms"] assert set(response.json["response"]["setup_methods"]) == {"email", "sms"} assert set(response.json["response"]["active_methods"]) == {"email", "password"} sms_sender = SmsSenderFactory.createSender("test") response = client_nc.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 state = response.json["response"]["state"] assert state # send invalid code response = client_nc.post( "/us-setup/" + state, json=dict(passcode=12344), headers=headers ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_PASSWORD_CODE" ) code = sms_sender.messages[0].split()[-1].strip(".") response = client_nc.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 200 assert response.json["response"]["chosen_method"] == "sms" assert response.json["response"]["phone"] == "+16505551212" # Verify sms in list of 'active' methods response = client_nc.get("/us-setup", headers=headers) assert response.status_code == 200 assert set(response.json["response"]["active_methods"]) == { "email", "sms", "password", } # now login with phone - send in different format than we set up with. headers = {"Accept": "application/json", "Content-Type": "application/json"} sms_sender = SmsSenderFactory.createSender("test") response = client_nc.post( "/us-signin/send-code", json=dict(identity="6505551212", chosen_method="sms"), headers=headers, ) assert response.status_code == 200 code = sms_sender.messages[0].split()[-1].strip(".") response = client_nc.post( "/us-signin?include_auth_token", json=dict(identity="matt@lp.com", passcode=code), headers=headers, ) assert response.status_code == 200 assert "authentication_token" in response.json["response"]["user"] @pytest.mark.settings( us_enabled_methods=["email", "sms"], user_identity_attributes=UIA_EMAIL_PHONE, ) def test_setup_json_no_session(app, client_nc, get_message, outbox): # Test that with normal config freshness is required and we can use auth_token # for that set_email(app) us_authenticate(client_nc) token = reset_fresh_auth_token(app, app.config["SECURITY_FRESHNESS"]) headers = { "Authentication-Token": token, "Accept": "application/json", "Content-Type": "application/json", } 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 # re-verify client_nc.post( "/us-verify/send-code", json=dict(identity="matt@lp.com", chosen_method="email"), headers=headers, ) matcher = re.match(r".*Token:(\d+).*", outbox[1].body, re.IGNORECASE | re.DOTALL) code = matcher.group(1) response = client_nc.post( "/us-verify?include_auth_token", json=dict(passcode=code), headers=headers ) assert response.status_code == 200 token = response.json["response"]["user"]["authentication_token"] headers["Authentication-Token"] = token # should work now response = client_nc.get("/us-setup", headers=headers) assert response.status_code == 200 @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" in response.data def test_setup_bad_token(app, client, get_message): set_email(app) headers = {"Accept": "application/json", "Content-Type": "application/json"} us_authenticate(client) # bogus state response = client.post( "/us-setup/not a token", json=dict(passcode=12345), headers=headers ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # same w/o json response = client.post( "/us-setup/not a token", data=dict(passcode=12345), follow_redirects=True ) assert get_message("API_ERROR") in response.data @pytest.mark.settings(us_setup_within="2 seconds") def test_setup_timeout(app, client, get_message): # Test setup timeout set_email(app) us_authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} sms_sender = SmsSenderFactory.createSender("test") app.security.us_setup_serializer = FakeSerializer(2.0) response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 state = response.json["response"]["state"] code = sms_sender.messages[0].split()[-1].strip(".") response = client.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_SETUP_EXPIRED", within=app.config["SECURITY_US_SETUP_WITHIN"] ) @pytest.mark.settings( us_enabled_methods=["email", "sms"], user_identity_attributes=UIA_EMAIL_PHONE, ) def test_unique_phone(app, client, get_message): # Test that us_phone_number is properly validated to be unique set_email(app, email="matt@lp.com") us_authenticate(client, identity="matt@lp.com") sms_sender = SmsSenderFactory.createSender("test") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), ) assert response.status_code == 200 state = response.json["response"]["state"] assert state code = sms_sender.messages[0].split()[-1].strip(".") response = client.post("/us-setup/" + state, json=dict(passcode=code)) assert response.status_code == 200 logout(client) set_email(app, email="joe@lp.com") us_authenticate(client, identity="joe@lp.com") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode() == get_message( "IDENTITY_ALREADY_ASSOCIATED", attr="us_phone_number", value="+16505551212" ) @pytest.mark.settings(freshness=timedelta(minutes=0)) def test_verify(app, clients, get_message): client = clients # Test setup when re-authenticate required # With freshness set to 0 - the first call should require reauth (by # redirecting); but the second should work due to grace period. set_email(app) us_authenticate(client) response = client.get("us-setup", follow_redirects=False) verify_url = response.location assert check_location(app, response.location, "/us-verify?next=/us-setup") logout(client) us_authenticate(client) response = client.get("us-setup", follow_redirects=True) form_response = response.data.decode("utf-8") assert "Reauthenticate" in form_response send_code_url = get_form_action(response, 1) # Send unknown method response = client.post( send_code_url, data=dict(identity="matt@lp.com", chosen_method="sms2"), follow_redirects=True, ) assert response.status_code == 200 assert b"Not a valid choice" in response.data # Verify using SMS sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = client.post( send_code_url, data=dict(identity="matt@lp.com", chosen_method="sms"), follow_redirects=True, ) assert response.status_code == 200 assert b"Code has been sent" in response.data code = sms_sender.messages[0].split()[-1].strip(".") response = client.post(verify_url, data=dict(passcode=code), follow_redirects=False) assert check_location(app, response.location, "/us-setup") 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 /us-setup set_email(app) us_authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} app.config["SECURITY_FRESHNESS"] = timedelta(minutes=0) response = client.get("us-setup", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] # figure out which methods are usable response = client.get("us-verify", headers=headers) assert response.json["response"]["available_methods"] == [ "password", "email", "authenticator", "sms", ] # code_methods should just contain active/setup methods. assert set(response.json["response"]["code_methods"]) == { "email", } response = client.post( "us-verify/send-code", json=dict(chosen_method="orb"), headers=headers, ) assert response.status_code == 400 # Verify using SMS sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = client.post( "us-verify/send-code", json=dict(chosen_method="sms"), headers=headers, ) assert response.status_code == 200 # Try bad code response = client.post("us-verify", json=dict(passcode=42), headers=headers) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_PASSWORD_CODE" ) assert response.json["response"]["field_errors"]["passcode"][0].encode( "utf-8" ) == get_message("INVALID_PASSWORD_CODE") response = client.post("us-verify", json=dict(passcode=None), headers=headers) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_PASSWORD_CODE" ) code = sms_sender.messages[0].split()[-1].strip(".") response = client.post("us-verify", json=dict(passcode=code), headers=headers) assert response.status_code == 200 app.config["SECURITY_FRESHNESS"] = timedelta(minutes=60) response = client.get("us-setup", headers=headers) assert response.status_code == 200 @pytest.mark.settings(freshness=timedelta(minutes=-1)) def test_setup_nofresh(app, client, get_message): set_email(app) us_authenticate(client) response = client.get("us-setup", follow_redirects=False) assert response.status_code == 200 response = client.get("us-verify") assert response.status_code == 404 @pytest.mark.settings(us_enabled_methods=["sms"]) def test_invalid_method(app, client, get_message): headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("/us-signin", headers=headers) assert response.json["response"]["available_methods"] == ["sms"] # verify json error response = client.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="email"), headers=headers, follow_redirects=True, ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_METHOD_NOT_AVAILABLE" ) # verify form error response = client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 assert get_message("US_METHOD_NOT_AVAILABLE") in response.data @pytest.mark.settings(us_enabled_methods=["sms", "email"]) def test_invalid_method_setup(app, client, get_message): set_email(app) us_authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("/us-setup", headers=headers) assert response.json["response"]["available_methods"] == ["sms", "email"] # verify json error response = client.post( "/us-setup", json=dict(email="matt@lp.com", chosen_method="authenticator"), headers=headers, follow_redirects=True, ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_METHOD_NOT_AVAILABLE" ) response = client.post( "/us-setup", data=dict(email="matt@lp.com", chosen_method="authenticators"), follow_redirects=True, ) assert get_message("US_METHOD_NOT_AVAILABLE") in response.data def test_setup_new_totp(app, client, get_message): # us-setup should generate a new totp secret for each setup # Verify existing codes no longer work set_email(app) us_authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} # Start by generating a good code - this will generate a new totp sms_sender = SmsSenderFactory.createSender("test") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), 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' # should no longer work sms_sender2 = SmsSenderFactory.createSender("test") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 state = response.json["response"]["state"] # Use old code response = client.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 400 # Use new code code = sms_sender2.messages[0].split()[-1].strip(".") response = client.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 200 assert response.json["response"]["chosen_method"] == "sms" assert response.json["response"]["phone"] == "+16505551212" def test_qrcode(app, client, get_message): # Test forms based authenticator setup - can't really parse QRcode - but can use # the key sent as part of the response. set_email(app, email="gal@lp.com") us_authenticate(client, identity="gal@lp.com") response = client.post("us-setup", data=dict(chosen_method="authenticator")) assert response.status_code == 200 # 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 totp = TOTP(totp_secret) code = totp.generate().token # get verify link e.g. /us-setup/{state} verify_url = get_form_action(response, 1) response = client.post(verify_url, data=dict(passcode=code), follow_redirects=True) assert response.status_code == 200 assert get_message("US_SETUP_SUCCESSFUL") in response.data def test_next(app, client, get_message): set_email(app) with capture_send_code_requests() as requests: response = client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 response = client.post( "/us-signin?next=/post_login", data=dict(identity="matt@lp.com", passcode=requests[0]["token"]), follow_redirects=False, ) assert "/post_login" in response.location logout(client) # Test form.next response = client.post( "/us-signin", data=dict( identity="matt@lp.com", passcode=requests[0]["token"], next="/post_login" ), follow_redirects=False, ) assert "/post_login" in response.location @pytest.mark.registerable() @pytest.mark.confirmable() @pytest.mark.settings(requires_confirmation_error_view="/confirm") def test_requires_confirmation_error_redirect(app, client): data = dict( email="jyl@lp.com", password="password", password_confirm="password", next="" ) response = client.post("/register", data=data, follow_redirects=True) set_email(app, email="jyl@lp.com") response = client.post( "/us-signin/send-code", data=dict(identity="jyl@lp.com", chosen_method="email"), follow_redirects=False, ) assert "/confirm" in response.location response = client.post( "/us-signin", data=dict(identity="jyl@lp.com", passcode="password"), follow_redirects=False, ) assert "/confirm" in response.location @pytest.mark.registerable() @pytest.mark.confirmable() def test_confirmable(app, client, get_message): # Verify can't log in if need confirmation. data = dict( email="dude@lp.com", password="password", password_confirm="password", next="" ) response = client.post("/register", data=data, follow_redirects=True) assert response.status_code == 200 set_email(app, email="dude@lp.com") response = client.post( "/us-signin/send-code", data=dict(identity="dude@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 assert get_message("CONFIRMATION_REQUIRED") in response.data # Verify not authenticated assert not is_authenticated(client, get_message) @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.registerable() @pytest.mark.recoverable() @pytest.mark.settings(password_required=False) def test_can_add_password(app, client, get_message): # Test that if register w/o a password, can use 'recover password' to assign one data = dict(email="trp@lp.com", password="", password_confirm="") response = client.post("/register", data=data, follow_redirects=True) assert b"Welcome trp@lp.com" in response.data logout(client) with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="trp@lp.com"), follow_redirects=True) token = requests[0]["token"] response = client.post( "/reset/" + token, data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, ) assert get_message("PASSWORD_RESET_NO_LOGIN") in response.data # authenticate with new password using standard/old login endpoint. response = authenticate( client, "trp@lp.com", "awesome sunset", follow_redirects=True ) assert b"Welcome trp@lp.com" in response.data logout(client) # authenticate with password and us-signin endpoint response = client.post( "/us-signin", data=dict(identity="trp@lp.com", passcode="awesome sunset"), follow_redirects=True, ) assert b"Welcome trp@lp.com" in response.data @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.registerable() @pytest.mark.changeable() @pytest.mark.settings(password_required=False) def test_change_empty_password(app, client, outbox): # test that if register w/o a password - can 'change' it. headers = {"Accept": "application/json", "Content-Type": "application/json"} data = dict(email="trp@lp.com", password="", password_confirm="") response = client.post("/register", data=data, follow_redirects=True) # should have been logged in since no confirmation # make sure requires a fresh authentication reset_fresh(client, app.config["SECURITY_FRESHNESS"]) data = dict( password="", new_password="awesome sunset", new_password_confirm="awesome sunset", ) response = client.post("/change", json=data) assert response.status_code == 401 assert response.json["response"]["reauth_required"] client.post( "/us-verify/send-code", json=dict(identity="trp@lp.com", chosen_method="email"), ) matcher = re.match(r".*Token:(\d+).*", outbox[1].body, re.IGNORECASE | re.DOTALL) code = matcher.group(1) response = client.post("/us-verify", json=dict(passcode=code)) assert response.status_code == 200 response = client.get("/change", headers=headers) assert not response.json["response"]["active_password"] response = client.get("/change") assert b"You do not" in response.data # now should be able to change response = client.post("/change", json=data) assert response.status_code == 200 logout(client) response = client.post( "/login", json=dict(email="trp@lp.com", password="awesome sunset") ) assert response.status_code == 200 @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.registerable() @pytest.mark.settings(password_required=False) def test_empty_password(app, client, get_message): # test that if no password - can't log in with empty password data = dict(email="trp@lp.com", password="", password_confirm="") response = client.post("/register", data=data, follow_redirects=True) logout(client) response = client.post("/us-signin", json=dict(identity="trp@lp.com", passcode="")) assert response.status_code == 400 assert response.json["response"]["field_errors"]["passcode"][0].encode( "utf-8" ) == get_message("INVALID_PASSWORD_CODE") assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_PASSWORD_CODE" ) @pytest.mark.settings( us_enabled_methods=["password"], user_identity_attributes=[ {"email": {"mapper": uia_email_mapper}}, {"username": {"mapper": lambda x: x}}, ], ) def test_regular_login(app, client, get_message): # If "password" in methods - then should be able to login with good-ol # login/password. # By having username - this also checks that we properly stop at the first # mapping. auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) response = client.post( "/us-signin", data=dict(identity="matt@lp.com", passcode="password", remember=True), follow_redirects=True, ) assert response.status_code == 200 assert client.get_cookie("remember_token") assert "password" in auths[0][1] assert is_authenticated(client, get_message) @pytest.mark.settings( us_enabled_methods=["sms"], user_identity_attributes=UIA_EMAIL_PHONE ) def test_regular_login_disallowed(app, client, get_message): # If "password" not in methods - then should not be able to use password response = client.post( "/us-signin", data=dict(identity="matt@lp.com", passcode="password", remember=True), follow_redirects=True, ) assert get_message("INVALID_PASSWORD_CODE") in response.data @pytest.mark.two_factor() @pytest.mark.settings(two_factor_required=True) def test_tf(app, client, get_message): # Test basic two-factor - default for signing in with password. response = client.post( "/us-signin", data=dict(identity="matt@lp.com", passcode="password", remember=True), follow_redirects=True, ) assert response.status_code == 200 message = b"Two-Factor authentication adds an extra layer of security" assert message in response.data assert b"Set up using SMS" in response.data sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Enter code to complete setup" in response.data code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data @pytest.mark.two_factor() @pytest.mark.settings(two_factor_required=True) def test_tf_link(app, client, get_message, outbox): # Verify two-factor required when using magic link set_email(app) response = client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 assert b"Sign In" in response.data matcher = re.match( r".*(http://[^\s*]*).*", outbox[0].body, re.IGNORECASE | re.DOTALL ) magic_link = matcher.group(1) response = client.get(magic_link, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") not in response.data message = b"Two-Factor authentication adds an extra layer of security" assert message in response.data @pytest.mark.two_factor() @pytest.mark.settings( two_factor_required=True, redirect_host="localhost:8081", redirect_behavior="spa", login_error_view="/login-error", ) def test_tf_link_spa(app, client, get_message, outbox): # Verify two-factor required when using magic link and SPA # This currently isn't supported and should redirect to an error. set_email(app) response = client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 assert b"Sign In" in response.data matcher = re.match( r".*(http://[^\s*]*).*", outbox[0].body, re.IGNORECASE | re.DOTALL ) magic_link = matcher.group(1) response = client.get(magic_link, follow_redirects=False) split = urlsplit(response.location) assert "localhost:8081" == split.netloc assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) assert qparams["tf_required"] == "1" assert qparams["email"] == "matt@lp.com" @pytest.mark.two_factor() @pytest.mark.settings( two_factor_required=True, user_identity_attributes=UIA_EMAIL_PHONE ) def test_tf_not(app, client, get_message): # Test basic two-factor - when first factor doesn't require second (e.g. SMS) # 1. sign in and setup TFA client.post( "/us-signin", data=dict(identity="matt@lp.com", passcode="password", remember=True), follow_redirects=True, ) sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") client.post("/tf-setup", data=data, follow_redirects=True) code = sms_sender.messages[0].split()[-1] client.post("/tf-validate", data=dict(code=code), follow_redirects=True) # 2. setup unified sign in with SMS response = client.post( "us-setup", data=dict(chosen_method="sms", phone="650-555-1212") ) verify_url = get_form_action(response, 1) code = sms_sender.messages[0].split()[-1].strip(".") response = client.post(verify_url, data=dict(passcode=code), follow_redirects=True) assert response.status_code == 200 assert get_message("US_SETUP_SUCCESSFUL") in response.data # 3. logout logout(client) # 4. sign in with SMS - should not require TFA client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="sms"), follow_redirects=True, ) code = sms_sender.messages[0].split()[-1].strip(".") response = client.post( "/us-signin", data=dict(identity="6505551212", passcode=code), follow_redirects=True, ) assert response.status_code == 200 # assert "sms" in auths[1][1] # Verify authenticated assert is_authenticated(client, get_message) @pytest.mark.settings(sms_service="bad") def test_bad_sender(app, client, get_message): # If SMS sender fails - make sure propagated # Test form, json, x signin, setup headers = {"Accept": "application/json", "Content-Type": "application/json"} set_phone(app) data = dict(identity="matt@lp.com", chosen_method="sms") response = client.post("/us-signin/send-code", data=data, follow_redirects=True) assert get_message("FAILED_TO_SEND_CODE") in response.data response = client.post("us-signin/send-code", json=data, headers=headers) assert response.status_code == 500 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "FAILED_TO_SEND_CODE" ) # Now test setup set_email(app) us_authenticate(client) data = dict(chosen_method="sms", phone="650-555-1212") response = client.post("us-setup", data=data) assert get_message("FAILED_TO_SEND_CODE") in response.data response = client.post("us-setup", json=data, headers=headers) assert response.status_code == 500 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "FAILED_TO_SEND_CODE" ) # Test us-verify data = dict(chosen_method="sms") response = client.post("/us-verify/send-code", data=data) assert get_message("FAILED_TO_SEND_CODE") in response.data response = client.post("us-verify/send-code", json=data) assert response.status_code == 500 assert response.json["response"]["field_errors"]["chosen_method"][0].encode( "utf-8" ) == get_message("FAILED_TO_SEND_CODE") @pytest.mark.registerable() def test_replace_send_code(app, get_message): pytest.importorskip("sqlalchemy") pytest.importorskip("flask_sqlalchemy") from flask_sqlalchemy import SQLAlchemy from flask_security.models import fsqla_v2 as fsqla from flask_security import Security, us_send_security_token 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): def us_send_security_token(self, method, **kwargs): assert method == "sms" us_send_security_token(self, method, **kwargs) with app.app_context(): db.create_all() ds = SQLAlchemyUserDatastore(db, User, Role) app.security = Security(app, datastore=ds) client = app.test_client() # since we don't use client fixture - have to add user data = dict(email="trp@lp.com", password="password", password_confirm="password") response = client.post("/register", data=data, follow_redirects=True) assert b"Welcome trp@lp.com" in response.data logout(client) set_phone(app, email="trp@lp.com") data = dict(identity="trp@lp.com", chosen_method="sms") response = client.post("/us-signin/send-code", data=data, follow_redirects=True) assert b"Code has been sent" in response.data with app.app_context(): db.engine.dispose() # sqlite wants everything cleaned up @pytest.mark.settings(us_enabled_methods=["password"]) def test_only_passwd(app, client, get_message): authenticate(client) response = client.get("us-setup") assert b"No method" in response.data headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("us-setup", headers=headers) assert response.json["response"]["available_methods"] == ["password"] assert not response.json["response"]["setup_methods"] @pytest.mark.settings(us_enabled_methods=["password", "authenticator"]) def test_passwd_and_authenticator(app, client, get_message): authenticate(client) response = client.get("us-setup") assert b"authenticator app" in response.data # Check that we get QRcode URL and no 'code sent' response = client.post("us-setup", data=dict(chosen_method="authenticator")) assert response.status_code == 200 assert b"Code has been sent" not in response.data assert b"Open an authenticator app" in response.data # verify png QRcode is present assert b"data:image/svg+xml;base64," in response.data headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("us-setup", headers=headers) assert response.json["response"]["available_methods"] == [ "password", "authenticator", ] assert response.json["response"]["setup_methods"] == ["authenticator"] def test_totp_generation(app, client, get_message): # Test that we generate a new totp on each setup of a different method # and that on failure to validate, the secret is NOT changed in the DB # and on successful validation, it is. headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client, email="dave@lp.com") with app.app_context(): user = app.security.datastore.find_user(email="dave@lp.com") ts = app.security.datastore.us_get_totp_secrets(user) assert "authenticator" not in ts response = client.post( "us-setup", json=dict(chosen_method="authenticator"), headers=headers ) assert response.status_code == 200 assert response.json["response"]["authr_issuer"] == "tests" assert response.json["response"]["authr_username"] == "dave@lp.com" assert "authr_key" in response.json["response"] state = response.json["response"]["state"] # Generate token from passed totp_secret and confirm setup totp_key = response.json["response"]["authr_key"] totp = TOTP(totp_key) code = totp.generate().token response = client.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 200 assert response.json["response"]["chosen_method"] == "authenticator" # success - totp_secret in DB should have been saved with app.app_context(): user = app.security.datastore.find_user(email="dave@lp.com") ts = app.security.datastore.us_get_totp_secrets(user) assert ( app.security._totp_factory.get_totp_pretty_key(ts["authenticator"]) == totp_key ) # Now setup SMS and verify that authenticator totp hasn't changed sms_sender = SmsSenderFactory.createSender("test") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 code = sms_sender.messages[0].split()[-1].strip(".") state = response.json["response"]["state"] response = client.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 200 assert response.json["response"]["chosen_method"] == "sms" assert response.json["response"]["phone"] == "+16505551212" # make sure authenticator totp hasn't changed. with app.app_context(): user = app.security.datastore.find_user(email="dave@lp.com") ts = app.security.datastore.us_get_totp_secrets(user) assert ( app.security._totp_factory.get_totp_pretty_key(ts["authenticator"]) == totp_key ) # Ok - setup again - but send invalid code - check totp in DB didn't change. response = client.post( "us-setup", json=dict(chosen_method="authenticator"), headers=headers ) assert response.status_code == 200 state = response.json["response"]["state"] totp_key = response.json["response"]["authr_key"] # validate with wrong code response = client.post( "/us-setup/" + state, json=dict(passcode=123345), headers=headers ) assert response.status_code == 400 # Make sure totp_secret in DB didn't change with app.app_context(): user = app.security.datastore.find_user(email="dave@lp.com") ts = app.security.datastore.us_get_totp_secrets(user) assert ( app.security._totp_factory.get_totp_pretty_key(ts["authenticator"]) != totp_key ) @pytest.mark.two_factor() @pytest.mark.settings( two_factor_required=True, user_identity_attributes=UIA_EMAIL_PHONE, two_factor_always_validate=False, ) def test_us_tf_validity(app, client, get_message): us_tf_authenticate(app, client, remember=True) assert client.get_cookie("tf_validity") logout(client) # logout does NOT remove this cookie assert client.get_cookie("tf_validity") # This time shouldn't require code data = dict(identity="gal@lp.com", passcode="password") response = client.post("/us-signin", json=data) assert response.json["meta"]["code"] == 200 assert is_authenticated(client, get_message) logout(client) data = dict(identity="gal2@lp.com", passcode="password") response = client.post("/us-signin", data=data, follow_redirects=True) assert b"Please enter your authentication code" in response.data response = client.post( "/us-signin", json=data, follow_redirects=True, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 assert response.json["response"]["tf_primary_method"] == "authenticator" assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" @pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) def test_us_verify_wan(app, client, get_message): # test get correct options when requiring a reauthentication and have wan keys # setup. headers = {"Accept": "application/json", "Content-Type": "application/json"} reg_2_keys(client) reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get("us-setup", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] assert response.json["response"]["has_webauthn_verify_credential"] # the us-verify form should have the webauthn verify form attached response = client.get("us-verify") assert b'action="/wan-verify"' in response.data app.config["SECURITY_WAN_ALLOW_AS_VERIFY"] = None response = client.get("us-setup", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] assert not response.json["response"]["has_webauthn_verify_credential"] # the us-verify form should NOT have the webauthn verify form attached response = client.get("us-verify") assert b'action="/wan-verify"' not in response.data def test_setup_delete(app, client, get_message): set_email(app) us_authenticate(client) response = client.get("us-setup") # Email should be in delete options since we just set that up. assert all( i in response.data for i in [b"delete_method-0", b"chosen_method-0", b"chosen_method-1"] ) response = client.post("us-setup", data=dict(delete_method="email")) response = client.get("us-setup") # All should be in possible setups. assert all( i in response.data for i in [b"chosen_method-0", b"chosen_method-1", b"chosen_method-2"] ) response = client.post("us-setup", json=dict(delete_method="email")) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_METHOD_NOT_AVAILABLE" ) def test_setup_delete_json(app, client, get_message): recorded = [] @us_profile_changed.connect_via(app) def pc(sender, user, methods, delete, **kwargs): if "sms" in methods: if delete: assert not user.us_phone_number recorded.append("delete") else: assert user.us_phone_number == "+16505551212" recorded.append("setup") headers = {"Accept": "application/json", "Content-Type": "application/json"} set_email(app) us_authenticate(client) response = client.get("us-setup", headers=headers) # Email should be in delete options since we just set that up. assert "email" in response.json["response"]["active_methods"] response = client.post("us-setup", json=dict(delete_method="email")) assert response.status_code == 200 response = client.get("us-setup", headers=headers) assert response.json["response"]["active_methods"] == ["password"] response = client.post("us-setup", json=dict(delete_method="email")) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_METHOD_NOT_AVAILABLE" ) # setup and delete SMS sms_sender = SmsSenderFactory.createSender("test") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 state = response.json["response"]["state"] assert state code = sms_sender.messages[0].split()[-1].strip(".") response = client.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 200 assert response.json["response"]["chosen_method"] == "sms" assert response.json["response"]["phone"] == "+16505551212" # verify SMS in active methods response = client.get("/us-setup", headers=headers) assert response.status_code == 200 assert set(response.json["response"]["active_methods"]) == {"sms", "password"} # delete SMS response = client.post("/us-setup", json=(dict(delete_method="sms"))) assert response.status_code == 200 response = client.get("/us-setup", headers=headers) assert response.json["response"]["active_methods"] == ["password"] assert recorded[0] == "setup" assert recorded[1] == "delete" def test_setup_delete_multi_json(app, client, get_message): recorded = [] @us_profile_changed.connect_via(app) def pc(sender, user, methods, delete, **kwargs): recorded.append((delete, methods)) headers = {"Accept": "application/json", "Content-Type": "application/json"} set_email(app) set_phone(app) us_authenticate(client) response = client.get("us-setup", headers=headers) # Email and sms should be in delete options since we just set that up. assert set(response.json["response"]["active_methods"]) == { "sms", "password", "email", } response = client.post("us-setup", json=dict(delete_method=["email", "sms"])) assert response.status_code == 200 response = client.get("us-setup", headers=headers) assert response.json["response"]["active_methods"] == ["password"] assert len(recorded) == 1 assert set(recorded[0][1]) == {"sms", "email"} @pytest.mark.settings(return_generic_responses=True) def test_generic_response(app, client, get_message): # test not-setup choice data = dict(identity="matt@lp.com", chosen_method="email") response = client.post("/us-signin/send-code", data=data, follow_redirects=True) assert get_message("GENERIC_US_SIGNIN") in response.data # for JSON still return 200 as if everything is fine and a code was sent. response = client.post("/us-signin/send-code", json=data) assert response.status_code == 200 assert not any(e in response.json["response"].keys() for e in ["error", "errors"]) # Correct method should return same thing set_phone(app) data = dict(identity="matt@lp.com", chosen_method="sms") response = client.post("/us-signin/send-code", data=data, follow_redirects=True) assert get_message("GENERIC_US_SIGNIN") in response.data # for JSON still return 200 as if everything is fine and a code was sent. response = client.post("/us-signin/send-code", json=data) assert response.status_code == 200 assert not any( e in response.json["response"].keys() for e in ["field_errors", "errors"] ) # Unknown identity should return same thing data = dict(identity="matt2@lp.com", chosen_method="email") response = client.post("/us-signin/send-code", data=data, follow_redirects=True) assert get_message("GENERIC_US_SIGNIN") in response.data # for JSON still return 200 as if everything is fine and a code was sent. response = client.post("/us-signin/send-code", json=data) assert response.status_code == 200 assert not any( e in response.json["response"].keys() for e in ["field_errors", "errors"] ) # # Now test us-signin itself # data = dict(identity="matt@lp.com", code="12345") response = client.post("/us-signin", data=data) assert get_message("GENERIC_AUTHN_FAILED") in response.data data = dict(identity="matt@lp.com", code="12345") response = client.post("/us-signin", json=data) assert response.status_code == 400 assert list(response.json["response"]["field_errors"].keys()) == [""] assert len(response.json["response"]["field_errors"][""]) == 1 assert response.json["response"]["field_errors"][""][0].encode( "utf-8" ) == get_message("GENERIC_AUTHN_FAILED") assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "GENERIC_AUTHN_FAILED" ) # same with unknown user data = dict(identity="matt2@lp.com", code="12345") response = client.post("/us-signin", data=data) assert get_message("GENERIC_AUTHN_FAILED") in response.data data = dict(identity="matt2@lp.com", code="12345") response = client.post("/us-signin", json=data) assert response.status_code == 400 assert list(response.json["response"]["field_errors"].keys()) == [""] assert len(response.json["response"]["field_errors"][""]) == 1 assert response.json["response"]["field_errors"][""][0].encode( "utf-8" ) == get_message("GENERIC_AUTHN_FAILED") assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "GENERIC_AUTHN_FAILED" ) # # Test /us-verify-link # set_email(app) with capture_send_code_requests() as requests: response = client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) uid = requests[0]["user"].fs_uniquifier code = requests[0]["login_token"] # Try bad/unknown id response = client.get("us-verify-link?id=98765&code=12345", follow_redirects=True) assert get_message("GENERIC_AUTHN_FAILED") in response.data # Try bad code response = client.get( f"us-verify-link?id={uid}&code=12345", follow_redirects=True, ) assert get_message("GENERIC_AUTHN_FAILED") in response.data # Try deactivated user with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.deactivate_user(user) app.security.datastore.commit() response = client.get( f"us-verify-link?id={uid}&code={code}", follow_redirects=True, ) assert get_message("GENERIC_AUTHN_FAILED") in response.data @pytest.mark.settings(url_prefix="/auth", us_signin_replaces_login=True) def test_propagate_next(app, client): # verify we propagate the ?next param all the way through a unified signin # Also test blueprint prefix since we rarely actually test that. set_phone(app) with capture_send_code_requests() as codes: response = client.get("profile", follow_redirects=True) assert "?next=/profile" in response.request.url signin_url = get_form_action(response, 0) sendcode_url = get_form_action(response, 1) response = client.post( sendcode_url, data=dict(identity="matt@lp.com", chosen_method="sms") ) data = dict(identity="matt@lp.com", passcode=codes[0]["login_token"]) response = client.post(signin_url, data=data, follow_redirects=False) assert "/profile" in response.location @pytest.mark.two_factor() @pytest.mark.settings(url_prefix="/auth", us_signin_replaces_login=True) def test_propagate_next_tf(app, client): # test next is propagated with a second factor response = client.post( "/auth/login", json=dict(identity="matt@lp.com", passcode="password") ) sms_sender = setup_tf_sms(client, url_prefix=app.config["SECURITY_URL_PREFIX"]) logout(client, endpoint="/auth/logout") response = client.get("/profile", follow_redirects=True) assert "?next=/profile" in response.request.url signin_url = get_form_action(response, 0) response = client.post( signin_url, data=dict(identity="matt@lp.com", passcode="password"), follow_redirects=True, ) sendcode_url = get_form_action(response, 0) response = client.post( sendcode_url, data=dict(code=sms_sender.messages[0].split()[-1]), follow_redirects=True, ) assert b"Profile Page" in response.data # Try form.next logout(client, endpoint="/auth/logout") response = client.post( "/auth/us-signin", data=dict(identity="matt@lp.com", passcode="password", next="/im-in"), follow_redirects=True, ) sendcode_url = get_form_action(response, 0) response = client.post( sendcode_url, data=dict(code=sms_sender.messages[0].split()[-1]), follow_redirects=False, ) assert "/im-in" in response.location @pytest.mark.app_settings(babel_default_locale="fr_FR") @pytest.mark.babel() def test_xlation(app, client, get_message_local): # Test method translation assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" set_email(app) us_authenticate(client) response = client.get("us-setup") # note we test against REAL translations - don't use same code as view uses. with app.test_request_context(): assert markupsafe.escape("SMS").encode() in response.data p = [ "Options de connexion actuellement actives : mot de passe et e-mail.", "Options de connexion actuellement actives : e-mail et mot de passe.", ] assert any(markupsafe.escape(s).encode() in response.data for s in p) @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.registerable() @pytest.mark.settings(password_required=False) @pytest.mark.app_settings(babel_default_locale="fr_FR") @pytest.mark.babel() def test_empty_password_xlate(app, client, get_message): # test that if no password (and no other setup method) we get correct xlated # template assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" data = dict(email="trp@lp.com", password="", password_confirm="") # register w/o password - this will automatically set up 'email' client.post("/register", data=data, follow_redirects=True) # will be auto-logged in since no confirmation response = client.get("us-setup") with app.test_request_context(): assert ( markupsafe.escape( "Options de connexion actuellement actives : e-mail." ).encode() in response.data ) # white-box testing - there are 2 places in us-setup code that set active methods response = client.post("us-setup", data=dict(delete_method="email")) with app.test_request_context(): assert ( markupsafe.escape( "Options de connexion actuellement actives : aucune." ).encode() in response.data ) response = client.get("us-setup") with app.test_request_context(): assert ( markupsafe.escape( "Options de connexion actuellement actives : aucune." ).encode() in response.data ) @pytest.mark.two_factor() @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(CSRF_COOKIE_NAME="XSRF-Token") def test_csrf_2fa_us_cookie(app, client): # Use XSRF-Token cookie for entire login sequence sms_sender = SmsSenderFactory.createSender("test") response = client.get( "/us-signin", data={}, headers={"Content-Type": "application/json"} ) assert client.get_cookie("XSRF-Token") csrf_token = response.json["response"]["csrf_token"] assert csrf_token == client.get_cookie("XSRF-Token").value # verify requires CSRF response = client.post( "/us-signin", json=dict(identity="gal@lp.com", passcode="password"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." response = client.post( "/us-signin", json=dict(identity="gal@lp.com", passcode="password"), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert b'"code": 200' in response.data session = get_session(response) assert session["tf_state"] == "ready" assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post( "/tf-validate", json=dict(code=code), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert response.status_code == 200 # verify original session csrf_token still works. response = client.post( "/json_auth", json=dict(label="label"), headers={"Content-Type": "application/json", "X-CSRF-Token": csrf_token}, ) assert response.status_code == 200 # use XSRF_Cookie to send in csrf_token response = client.post( "/json_auth", json=dict(label="label"), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert response.status_code == 200 assert response.json["label"] == "label" @pytest.mark.settings(us_enabled_methods=["password", "email"]) def test_us_setup_email(app, client, get_message): set_email(app) us_authenticate(client) chosen_methods_choices = [] def recorder(template, us_setup_form, **kwargs): chosen_methods_choices.extend(us_setup_form.chosen_method.choices) return "ok" app.security.render_template = recorder client.get("/us-setup") assert len(chosen_methods_choices) == 0 @pytest.mark.settings(us_enabled_methods=["email", "authenticator"]) def test_us_setup_authenticator(app, client, get_message): set_email(app) us_authenticate(client) chosen_methods_choices = [] def recorder(template, us_setup_form, **kwargs): chosen_methods_choices.extend(us_setup_form.chosen_method.choices) return "ok" app.security.render_template = recorder client.get("/us-setup") assert len(chosen_methods_choices) == 1 assert chosen_methods_choices[0][0] == "authenticator" @pytest.mark.settings(us_enabled_methods=["email", "sms"]) def test_us_setup_sms(app, client, get_message): set_email(app) us_authenticate(client) chosen_methods_choices = [] def recorder(template, us_setup_form, **kwargs): chosen_methods_choices.extend(us_setup_form.chosen_method.choices) return "ok" app.security.render_template = recorder client.get("/us-setup") assert len(chosen_methods_choices) == 1 assert chosen_methods_choices[0][0] == "sms" @pytest.mark.settings(us_enabled_methods=["email", "sms", "authenticator"]) def test_us_setup_authenticator_sms(app, client, get_message): set_email(app) us_authenticate(client) chosen_methods_choices = [] def recorder(template, us_setup_form, **kwargs): chosen_methods_choices.extend(us_setup_form.chosen_method.choices) return "ok" app.security.render_template = recorder client.get("/us-setup") assert len(chosen_methods_choices) == 2 assert chosen_methods_choices[0][0] == "authenticator" assert chosen_methods_choices[1][0] == "sms" flask-security-5.7.1/tests/test_utils.py000066400000000000000000000347241511046741400204210ustar00rootroot00000000000000""" test_utils ~~~~~~~~~~ Test utils :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from __future__ import annotations from contextlib import contextmanager import re import time from flask.json.tag import TaggedJSONSerializer from flask.signals import message_flashed from flask_security import ( Security, SmsSenderBaseClass, SmsSenderFactory, UserMixin, ) from flask_security.signals import ( login_instructions_sent, reset_password_instructions_sent, tf_security_token_sent, user_registered, us_security_token_sent, username_recovery_email_sent, ) from flask_security.utils import hash_data, hash_password from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer from werkzeug.http import parse_cookie _missing = object def authenticate( client, email="matt@lp.com", password="password", endpoint=None, csrf=False, **kwargs, ): data = dict(email=email, password=password, remember="y") if csrf: response = client.get(endpoint or "/login") data["csrf_token"] = get_form_input_value(response, "csrf_token") return client.post(endpoint or "/login", data=data, **kwargs) def json_authenticate(client, email="matt@lp.com", password="password", endpoint=None): data = dict(email=email, password=password) # Get auth token always ep = endpoint or "/login?include_auth_token" return client.post(ep, content_type="application/json", json=data) def is_authenticated(client, get_message, auth_token=None): # Return True is 'client' is authenticated. # Return False if not # Raise ValueError not certain... headers = {"accept": "application/json"} if auth_token: headers["Authentication-Token"] = auth_token response = client.get("/profile", headers=headers) if response.status_code == 200: return True if response.status_code == 401 and response.json["response"]["errors"][0].encode( "utf-8" ) == get_message("UNAUTHENTICATED"): return False raise ValueError("Failed to figure out if authenticated") def check_location(app, location, expected_base): # verify response location. Historically this can be absolute or relative based # on configuration. As of 5.4 and Werkzeug 2.1 it is always relative return location == expected_base def verify_token(client_nc, token, status=None): # Use passed auth token in API that requires auth and verify status. # Pass in a client_nc to get valid results. response = client_nc.get( "/token", headers={"Content-Type": "application/json", "Authentication-Token": token}, ) if status: assert response.status_code == status else: assert b"Token Authentication" in response.data def logout(client, endpoint=None, **kwargs): return client.get(endpoint or "/logout", **kwargs) def json_logout(client, token, endpoint=None): return client.post( endpoint or "/logout", headers={ "Content-Type": "application/json", "Accept": "application/json", "Authentication-Token": token, }, ) def get_csrf_token(client): response = client.get( "/login", data={}, headers={"Accept": "application/json"}, ) return response.json["response"]["csrf_token"] def get_session(response): """Return session cookie contents. This a base64 encoded json. Returns a dict """ # Alas seems like if there are multiple set-cookie headers - we are on our own for index, h in enumerate(response.headers): if h[0] == "Set-Cookie": cookie = parse_cookie(response.headers[index][1]) encoded_cookie = cookie.get("session", None) if encoded_cookie: serializer = URLSafeTimedSerializer( "secret", serializer=TaggedJSONSerializer() ) val = serializer.loads_unsafe(encoded_cookie) return val[1] def setup_tf_sms(client, url_prefix=None, csrf_token=None): # Simple setup of SMS as a second factor and return the sender so caller # can get codes. SmsSenderFactory.senders["test"] = SmsTestSender sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661188", csrf_token=csrf_token) response = client.post("/".join(filter(None, (url_prefix, "tf-setup"))), json=data) assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post( "/".join(filter(None, (url_prefix, "tf-validate"))), json=dict(code=code, csrf_token=csrf_token), ) assert response.status_code == 200 return sms_sender def get_existing_session(client): cookie = client.get_cookie("session") if cookie: serializer = URLSafeTimedSerializer("secret", serializer=TaggedJSONSerializer()) val = serializer.loads_unsafe(cookie.value) return val[1] def reset_fresh(client, within): # Assumes client authenticated. # Upon return the NEXT request if protected with a freshness check # will require a fresh authentication. with client.session_transaction() as sess: old_paa = sess["fs_paa"] - within.total_seconds() - 100 sess["fs_paa"] = old_paa sess.pop("fs_gexp", None) return old_paa def reset_fresh_auth_token(app, within, email="matt@lp.com"): # Assumes client authenticated. # Returns a new auth token that will force the NEXT request, # if protected with a freshness check to require a fresh authentication with app.test_request_context("/"): user = app.security.datastore.find_user(email=email) tdata = dict(ver=str(5)) if hasattr(user, "fs_token_uniquifier"): tdata["uid"] = str(user.fs_token_uniquifier) else: tdata["uid"] = str(user.fs_uniquifier) tdata["fs_paa"] = time.time() - within.total_seconds() - 100 tdata["exp"] = int(app.config.get("SECURITY_TOKEN_EXPIRE_TIMESTAMP")(user)) return app.security.remember_token_serializer.dumps(tdata) def get_form_action(response, ordinal=0): # Return the URL that the form WOULD post to - this is useful to check # how our templates actually work (e.g. propagation of 'next') matcher = re.findall( r'(?:
]*id="{field_id}"[^>]*)">') def get_form_input_value(response, field_id): # return 'value' of field with the id == field_id or None if not found return _parse_form_input( response, f']*id="{field_id}"[^>]*value="([^"]*)">' ) def check_xlation(app, locale): """Return True if locale is loaded""" with app.test_request_context(): domain = app.security.i18n_domain xlations = domain.get_translations() if not xlations: return False # Flask-Babel doesn't populate _info as Flask-BabelEx did - so look in first # string which is catalog info. matcher = re.search(r"Language:\s*(\w+)", xlations._catalog[""]) return matcher.group(1) == locale def create_roles(ds): roles = [ ("admin", ["full-read", "full-write", "super"]), ("editor", ["full-read", "full-write"]), ("author", ["full-read", "my-write"]), ("simple", None), ] for role in roles: if hasattr(ds.role_model, "permissions") and role[1]: ds.create_role(name=role[0], permissions=role[1]) else: ds.create_role(name=role[0]) ds.commit() def create_users(app, ds, count=None): users = [ ("matt@lp.com", "matt", "password", ["admin"], True, 123456, None), ("joe@lp.com", "joe", "password", ["editor"], True, 234567, None), ("dave@lp.com", "dave", "password", ["admin", "editor"], True, 345678, None), ("jill@lp.com", "jill", "password", ["author"], True, 456789, None), ("tiya@lp.com", "tiya", "password", [], False, 567890, None), ("gene@lp.com", "gene", "password", ["simple"], True, 889900, None), ("jess@lp.com", "jess", None, [], True, 678901, None), ("gal@lp.com", "gal", "password", ["admin"], True, 112233, "sms"), ("gal2@lp.com", "gal2", "password", ["admin"], True, 223311, "authenticator"), ("gal3@lp.com", "gal3", "password", ["admin"], True, 331122, "email"), ] count = count or len(users) for u in users[:count]: pw = u[2] if pw is not None: pw = hash_password(pw) roles = [ds.find_or_create_role(rn) for rn in u[3]] ds.commit() totp_secret = None if app.config.get("SECURITY_TWO_FACTOR", None) and u[6]: totp_secret = app.security._totp_factory.generate_totp_secret() user = ds.create_user( email=u[0], username=u[1], password=pw, active=u[4], security_number=u[5], tf_primary_method=u[6], tf_totp_secret=totp_secret, ) ds.commit() for role in roles: ds.add_role_to_user(user, role) ds.commit() def populate_data(app, user_count=None): ds = app.security.datastore with app.app_context(): create_roles(ds) create_users(app, ds, user_count) def init_app_with_options(app, datastore, **options): security_args = options.pop("security_args", {}) app.config.update(**options) app.security = Security(app, datastore=datastore, **security_args) populate_data(app) @contextmanager def capture_queries(datastore): from sqlalchemy import event queries = [] @event.listens_for(datastore.db.session, "do_orm_execute") def _do_orm_execute(orm_execute_state): queries.append(orm_execute_state) yield queries class SmsTestSender(SmsSenderBaseClass): messages: list[str] = [] count = 0 # This looks strange because we need class variables since test need to access a # sender but the actual sender is instantiated low down in SMS code. def __init__(self): super().__init__() SmsTestSender.count = 0 SmsTestSender.messages = [] def send_sms(self, from_number, to_number, msg): SmsTestSender.messages.append(msg) SmsTestSender.count += 1 return def get_count(self): return SmsTestSender.count class SmsBadSender(SmsSenderBaseClass): def send_sms(self, from_number, to_number, msg): raise ValueError(f"Unknown number: {to_number}") @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): data["email"] = data["user"].email 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_username_recovery_requests(): """Testing utility for capturing username recovery requests.""" recovery_requests = [] def _on(app, **data): recovery_requests.append(data) username_recovery_email_sent.connect(_on) try: yield recovery_requests finally: username_recovery_email_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) @contextmanager def capture_send_code_requests(): # Easy way to get token/code required for code logins # either second factor or us_signin login_requests = [] def _on(app, **data): assert all(v in data for v in ["user", "method", "login_token"]) assert isinstance(data["user"], UserMixin) login_requests.append(data) us_security_token_sent.connect(_on) tf_security_token_sent.connect(_on) try: yield login_requests finally: us_security_token_sent.disconnect(_on) tf_security_token_sent.disconnect(_on) def get_auth_token_version_3x(app, user): """ Copy of algorithm that generated user token in version 3.x """ data = [str(user.id), hash_data(user.password)] if hasattr(user, "fs_uniquifier"): data.append(user.fs_uniquifier) return app.security.remember_token_serializer.dumps(data) def get_auth_token_version_4x(app, user): """Copy of algorithm that generated user token in version 4.x- 5.4""" data = [str(user.fs_uniquifier)] return app.security.remember_token_serializer.dumps(data) class FakeSerializer: def __init__(self, age=None, invalid=False): self.age = age self.invalid = invalid def loads(self, token, max_age): if self.age: assert max_age == self.age raise SignatureExpired("expired") if self.invalid: raise BadSignature("bad") def loads_unsafe(self, token): return None, None def dumps(self, state): return "heres your state" def convert_bool_option(v): # Used for command line options to convert string to bool if str(v).lower() in ["true"]: return True elif str(v).lower() in ["false"]: return False return v flask-security-5.7.1/tests/test_webauthn.py000066400000000000000000002023321511046741400210660ustar00rootroot00000000000000""" test_webauthn ~~~~~~~~~~~~~~~~~~~ WebAuthn tests :copyright: (c) 2021-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from base64 import urlsafe_b64encode import copy import datetime import json import re import pytest from tests.test_two_factor import tf_in_session from tests.test_utils import ( FakeSerializer, authenticate, capture_flashes, check_location, get_existing_session, get_form_action, get_form_input_value, is_authenticated, json_authenticate, logout, reset_fresh, setup_tf_sms, verify_token, ) from flask_security import ( WebauthnUtil, user_authenticated, wan_registered, wan_deleted, ) # We can't/don't test the actual client-side javascript and browser APIs - so # to create reproducible tests, use view_scaffold, set breakpoints in the views and # cut-and-paste the responses. That requires that 'challenge' and 'rp_origin' be # identical between view_scaffold and tests here. CHALLENGE = "smCCiy_k2CqQydSQ_kPEjV5a2d0ApfatcpQ1aXDmQPo" REG_DATA1 = { "id": "wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc", "rawId": "wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc", "type": "public-key", "response": { "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NB" "cPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAQAAAAAAAAAAAAAAAAAAA" "AAAIMFFKjTo2N-XXE_r6YpGaWcfk_dTYyHuD6q1fI-42DznpQECAy" "YgASFYIFRipoWMEiDuCtLUvSlqCFZBqxvUuNqZKavlWgvN2BK8Il" "ggLOV4eez9k0det5oIZGyKanGkmWa0hygnjjFmf8Rep6c", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYzIxR" "FEybDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjRk" "V4WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NT" "AwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0", "transports": ["usb"], }, "extensions": '{"credProps": {}}', } SIGNIN_DATA1 = { "id": "wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc", "rawId": "wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc", "type": "public-key", "response": { "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAABQ==", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYzIxRFEy" "bDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjRkV4" "WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAw" "MSIsImNyb3NzT3JpZ2luIjpmYWxzZX0=", "signature": "MEUCIH5VdRXxfnoxfrVk72gvWAn91QH-l2UrIohk5YOWi9XpAiEAn6f9oHtFS" "68HVf6K_Ku0L33C0sID2HzpJWSiTNgJlbU=", }, "assertionClientExtensions": "{}", } REG_DATA2 = { "id": "lpMv8FTVHVSxteQJ3N4azlSxXiBJADA7IK-NleETceZYODy51_Cqt7Rx6pfVP1BI", "rawId": "lpMv8FTVHVSxteQJ3N4azlSxXiBJADA7IK-NleETceZYODy51_Cqt7Rx6pfVP1BI", "type": "public-key", "response": { "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjCSZYN5YgOj" "Gh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2PFAAAAAgAAAAAAAAAAA" "AAAAAAAAAAAMJaTL_BU1R1UsbXkCdzeGs5UsV4gSQAwOyCvjZXhE3" "HmWDg8udfwqre0ceqX1T9QSKUBAgMmIAEhWCCWky_wVNUdVL" "G15AncLU8mBQCtY10BjnSDoOUlRjkU1CJYIIA1U9vNDpZ" "TihC2x0CxRZ-trF_zazYosuEqYdHSOIjZoWtjcmVkUHJvdGVjdAI", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoi" "YzIxRFEybDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UV" "hCbVlYUmpjRkV4WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6L" "y9sb2NhbGhvc3Q6NTAwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0", "transports": ["nfc", "usb"], }, "extensions": '{"credProps": {"rk": True}}', } # This has user_verification=True - i.e. a multi-factor capable key REG_DATA_UV = { "id": "s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy", "rawId": "s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy", "type": "public-key", "response": { "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjC" "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2PFAAAABAAAAA" "AAAAAAAAAAAAAAAAAAMLN8WaXxstGR_rEpH8SLIAocG5MPztIzhbWXi" "dS11DBGvGrRtaBLJDaphSQn4CmRsqUBAgMmIAEhWCCzfFml8bLRkf" "6xKR_EUnaoI333MuxRlv5-LwojDibdTyJYIFMifFwn-RfkDDgsTHF" "jWgE6bld-Jc4nhFMTkQja9P8IoWtjcmVkUHJvdGVjdAI", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYzI" "xRFEybDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlY" "UmpjRkV4WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2Nhb" "Ghvc3Q6NTAwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0", "transports": ["nfc", "usb"], }, "extensions": '{"credProps":{"rk":true}}', } SIGNIN_DATA_UV = { "id": "s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy", "rawId": "s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy", "type": "public-key", "response": { "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYzIxRFEy" "bDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjRkV4W" "VZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMSI" "sImNyb3NzT3JpZ2luIjpmYWxzZX0=", "signature": "MEUCIQDR0m9Ob4nqVGiAPUf1Tu5XohDh2frl1LJ6G41GURlUIgIgKUPfkw" "AjP2863L2nDhcR2EKqoGEQLqlQ5xymZstyO6o=", }, "assertionClientExtensions": "{}", } REG_DATA_UH = { "id": "rHb1OXVM--dgGcWg0u3cfomyc-Tu4l4kK8GjVkS8bms-foXmBAlWHyTzuhgGgCnx", "rawId": "rHb1OXVM--dgGcWg0u3cfomyc-Tu4l4kK8GjVkS8bms-foXmBAlWHyTzuhgGgCnx", "type": "public-key", "response": { "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjCSZYN5YgOjGh0NBc" "PZHZgW4_krrmihjLHmVzzuoMd" "l2PFAAAAAgAAAAAAAAAAAAAAAAAAAAAAMKx29Tl1TPvnYBnFoNLt3H6J" "snPk7uJeJCvBo1ZEvG5rPn6F" "5gQJVh8k87oYBoAp8aUBAgMmIAEhWCCsdvU5dUz752AZxaDSyN-ocBL" "Bo99GevEWTnUxSkMRICJYIILE" "DLF8cQNM5l6ZgDxIYpvU88xgbq44lmR6oCBbNaHhoWtjcmVkUHJvdG" "VjdAI", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYzIx" "RFEybDVYMnN5UTNGUmVXUlRVVjl" "yVUVWcVZqVmhNbVF3UVhCbVlYUmpjRkV4WVZoRWJWRlFidyIsIm9yaWdpbi" "I6Imh0dHA6Ly9sb2NhbGhvc3" "Q6NTAwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0", "transports": ["nfc", "usb"], }, "extensions": '{"credProps":{"rk": true}}"', } SIGNIN_DATA_UH = { "id": "rHb1OXVM--dgGcWg0u3cfomyc-Tu4l4kK8GjVkS8bms-foXmBAlWHyTzuhgGgCnx", "rawId": "rHb1OXVM--dgGcWg0u3cfomyc-Tu4l4kK8GjVkS8bms-foXmBAlWHyTzuhgGgCnx", "type": "public-key", "response": { "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYzIxRFEy" "bDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjR" "kV4WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q" "6NTAwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0=", "signature": "MEUCIQCbKwaQv_GzrWfc0nVXqhe6WZs_5Sb2b7xHC9iDW9aHeQIgF2PlfM7FdyV" "xcPhofekJjLBgDMTbK4mwIWHgiExZ54s=", "userHandle": "NTgxMTU3YmM2MGU3NGM1OTg0OTBjYTI1ZTgwNjc4MDY=", }, "assertionClientExtensions": "{}", } class HackWebauthnUtil(WebauthnUtil): def generate_challenge(self, nbytes=None): return CHALLENGE def origin(self): # This is from view_scaffold return "http://localhost:5001" pytestmark = pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) def _register_start( client, name="testr1", usage="secondary", endpoint="wan-register", csrf_token=None ): response = client.post( endpoint, data=dict(name=name, usage=usage, csrf_token=csrf_token) ) matcher = re.match( r".*handleRegister\(\'(.*)\'\).*", response.data.decode("utf-8"), re.IGNORECASE | re.DOTALL, ) register_options = json.loads(matcher.group(1)) response_url = get_form_action(response) return register_options, response_url def _register_start_json(client, name="testr1", usage="secondary", csrf_token=None): response = client.post( "wan-register", json=dict(name=name, usage=usage, csrf_token=csrf_token) ) register_options = response.json["response"]["credential_options"] response_url = f'wan-register/{response.json["response"]["wan_state"]}' return register_options, response_url def reg_2_keys(client): # Register 2 keys - one first, one secondary # This can be used by other tests outside this module. authenticate(client) register_options, response_url = _register_start_json( client, name="first", usage="first" ) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA_UV))) assert response.status_code == 200 register_options, response_url = _register_start_json( client, name="second", usage="secondary" ) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 return { "first": {"id": REG_DATA_UV["id"], "signin": SIGNIN_DATA_UV}, "secondary": {"id": REG_DATA1["id"], "signin": SIGNIN_DATA1}, } def reg_first_key(client, csrf_token=None): # Register a primary key - assumes already authenticated # This can be used by other tests outside this module. register_options, response_url = _register_start_json( client, name="first", usage="first", csrf_token=csrf_token ) response = client.post( response_url, json=dict(credential=json.dumps(REG_DATA_UV), csrf_token=csrf_token), ) assert response.status_code == 200 return {"id": REG_DATA_UV["id"], "signin": SIGNIN_DATA_UV} def _signin_start( client, identity=None, endpoint="wan-signin", csrf_token=None, ): response = client.post( endpoint, data=dict(identity=identity, csrf_token=csrf_token) ) matcher = re.match( r".*handleSignin\(\'(.*)\'\).*", response.data.decode("utf-8"), re.IGNORECASE | re.DOTALL, ) signin_options = json.loads(matcher.group(1)) response_url = get_form_action(response) return signin_options, response_url def _signin_start_json(client, identity=None, remember=False, endpoint="wan-signin"): headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.post( endpoint, headers=headers, json=dict(identity=identity, remember=remember) ) signin_options = response.json["response"]["credential_options"] response_url = f'wan-signin/{response.json["response"]["wan_state"]}' return signin_options, response_url, response.json def wan_signin(client, identity, signin_data, wan_signin_url): # perform complete sign in - useful for tests outside this module. signin_options, response_url = _signin_start( client, identity, endpoint=wan_signin_url ) response = client.post( response_url, data=dict(credential=json.dumps(signin_data)), follow_redirects=True, ) assert response.status_code == 200 return response def reset_signcount(app, email, keyname): # Due to replay attack prevention, we can only use a key once since the server # increments the sign_count and we can't do that on the client side! with app.app_context(): user = app.security.datastore.find_user(email=email) cred = [c for c in user.webauthn if c.name == keyname][0] cred.sign_count = cred.sign_count - 1 app.security.datastore.put(cred) app.security.datastore.commit() def test_basic(app, clients, get_message): auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) @wan_registered.connect_via(app) def pc(sender, user, name, **extra_args): assert name == "testr1" assert len(user.webauthn) == 1 authenticate(clients) response = clients.get("/wan-register") # default config allows for both primary and secondary usage # so form should have selector assert get_form_input_value(response, "usage-0") assert get_form_input_value(response, "usage-1") # post with no name response = clients.post("/wan-register", data=dict()) assert get_message("WEBAUTHN_NAME_REQUIRED") in response.data register_options, response_url = _register_start(clients, usage="first") assert register_options["rp"]["name"] == "My Flask App" assert register_options["user"]["name"] == "matt@lp.com" assert not register_options["excludeCredentials"] assert register_options["authenticatorSelection"]["residentKey"] == "preferred" assert register_options["extensions"]["credProps"] # Register using the static data above response = clients.post( response_url, data=dict(credential=json.dumps(REG_DATA1)), follow_redirects=True ) assert response.status_code == 200 assert get_message("WEBAUTHN_REGISTER_SUCCESSFUL", name="testr1") in response.data assert b"testr1" in response.data # sign in - simple case use identity so we get back allowCredentials logout(clients) signin_options, response_url = _signin_start(clients, "matt@lp.com") assert signin_options["timeout"] == app.config["SECURITY_WAN_SIGNIN_TIMEOUT"] assert signin_options["userVerification"] == "preferred" allow_credentials = signin_options["allowCredentials"] assert len(allow_credentials) == 1 assert allow_credentials[0]["id"] == REG_DATA1["id"] assert allow_credentials[0]["transports"] == ["usb"] response = clients.post( response_url, data=dict(credential=json.dumps(SIGNIN_DATA1)), follow_redirects=True, ) assert response.status_code == 200 assert b"Welcome matt@lp.com" in response.data assert len(auths) == 2 assert auths[1][1] == ["webauthn"] # verify actually logged in response = clients.get("/profile", follow_redirects=False) assert response.status_code == 200 def test_basic_json(app, clients, get_message): headers = {"Accept": "application/json", "Content-Type": "application/json"} auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) authenticate(clients) # post with no name response = clients.post("/wan-register", json=dict()) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_NAME_REQUIRED" ) register_options, response_url = _register_start_json(clients, usage="first") assert register_options["rp"]["name"] == "My Flask App" assert register_options["user"]["name"] == "matt@lp.com" # Register using the static data above response = clients.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 # reset lastuse_datetime so we can verify signing in correctly alters it fake_dt = datetime.datetime(2020, 4, 7, 9, 27) with app.app_context(): user = app.security.datastore.find_user(email="matt@lp.com") for cred in user.webauthn: cred.lastuse_datetime = fake_dt app.security.datastore.put(cred) app.security.datastore.commit() if hasattr(app.security.datastore.db, "close_db") and callable( app.security.datastore.db.close_db ): app.security.datastore.db.close_db(None) response = clients.get("/wan-register", headers=headers) active_creds = response.json["response"]["registered_credentials"] assert active_creds[0]["name"] == "testr1" assert datetime.datetime.fromisoformat(active_creds[0]["lastuse"]) == fake_dt # sign in - simple case use identity so we get back allowCredentials logout(clients) signin_options, response_url, rjson = _signin_start_json(clients, "matt@lp.com") assert signin_options["userVerification"] == "preferred" allow_credentials = signin_options["allowCredentials"] assert len(allow_credentials) == 1 assert allow_credentials[0]["id"] == REG_DATA1["id"] assert "user" not in rjson["response"] response = clients.post( f"{response_url}?include_auth_token", json=dict(credential=json.dumps(SIGNIN_DATA1)), ) assert response.status_code == 200 assert response.json["response"]["user"]["email"] == "matt@lp.com" assert auths[1][1] == ["webauthn"] # verify actually logged in assert is_authenticated(clients, get_message) verify_token(clients, response.json["response"]["user"]["authentication_token"]) # fetch credentials and verify lastuse was updated response = clients.get("/wan-register", headers=headers) active_creds = response.json["response"]["registered_credentials"] assert datetime.datetime.fromisoformat(active_creds[0]["lastuse"]) != fake_dt assert active_creds[0]["transports"] == ["usb"] assert active_creds[0]["usage"] == "first" logout(clients) # verify that unknown identities are just ignored when USER_HINTS is True response = clients.post("/wan-signin", json=dict(identity="whoami@lp.com")) assert response.status_code == 200 @pytest.mark.settings(wan_allow_user_hints=False) def test_basic_json_nohints(app, client, get_message): # Test that with no hints allowed, we don't get any credentials and we can still # sign in. authenticate(client) register_options, response_url = _register_start_json(client, usage="first") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 # With no hints we default to requiring a resident key # With allow as primary we default to requiring a cross-platform key assert ( register_options["authenticatorSelection"]["authenticatorAttachment"] == "cross-platform" ) assert register_options["authenticatorSelection"]["residentKey"] == "required" logout(client) signin_options, response_url, rjson = _signin_start_json(client, "matt@lp.com") allow_credentials = signin_options["allowCredentials"] assert len(allow_credentials) == 0 assert "user" not in rjson["response"] response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1)), ) assert response.status_code == 200 assert response.json["response"]["user"]["email"] == "matt@lp.com" def test_usage(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json(client, usage="secondary") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1)), ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_CREDENTIAL_WRONG_USAGE" ) def test_constraints(app, clients, get_message): """Test that nickname is unique for a given user but different users can have the same nickname. Also that credential_id is unique across the app. """ authenticate(clients) register_options, response_url = _register_start_json(clients, name="testr3") response = clients.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 # register same name again response = clients.post("wan-register", json=dict(name="testr3")) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_NAME_INUSE", name="testr3" ) logout(clients) # Different user - should get credential id in use error authenticate(clients, email="joe@lp.com") register_options, response_url = _register_start_json(clients, name="testr3") response = clients.post(response_url, json=dict(credential=json.dumps(REG_DATA2))) assert response.status_code == 200 # Try to register with identical credential ID as other user register_options, response_url = _register_start_json(clients, name="testr4") response = clients.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_CREDENTIAL_ID_INUSE" ) def test_bad_data_register(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") # first try mangling json - should get API_ERROR response = client.post(response_url, json=dict(credential='"hi there"')) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) response = client.post(response_url, json=dict(credential="")) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # Now pass incorrect keys bad_register = copy.deepcopy(REG_DATA1) del bad_register["rawId"] response = client.post(response_url, json=dict(credential=json.dumps(bad_register))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # now muck with attestation - should get VERIFY ERROR bad_register = copy.deepcopy(REG_DATA1) bad_register["rawId"] = "unknown" response = client.post(response_url, json=dict(credential=json.dumps(bad_register))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_NO_VERIFY", cause="id and raw_id were not equivalent" ) # same with forms with capture_flashes() as flashes: response = client.post( response_url, data=dict(credential=json.dumps(bad_register)), follow_redirects=False, ) assert response.status_code == 302 assert "/wan-register" in response.location assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "WEBAUTHN_NO_VERIFY", cause="id and raw_id were not equivalent" ) def test_bad_data_signin(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json(client, usage="first") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") response = client.post(response_url, json=dict(credential='"hi there"')) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) response = client.post(response_url, json=dict(credential="")) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # Now pass incorrect keys bad_signin = copy.deepcopy(SIGNIN_DATA1) del bad_signin["rawId"] response = client.post(response_url, json=dict(credential=json.dumps(bad_signin))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # now muck with attestation - should get VERIFY ERROR bad_signin = copy.deepcopy(SIGNIN_DATA1) bad_signin["response"]["signature"] = bad_signin["response"]["signature"].replace( "M", "N" ) response = client.post(response_url, json=dict(credential=json.dumps(bad_signin))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_NO_VERIFY", cause="Could not verify authentication signature" ) def test_delete(app, clients, get_message): @wan_deleted.connect_via(app) def pc(sender, user, name, **extra_args): assert name == "testr3" assert len(user.webauthn) == 1 authenticate(clients) register_options, response_url = _register_start(clients, name="testr3") response = clients.post( response_url, data=dict(credential=json.dumps(REG_DATA1)), follow_redirects=True ) assert response.status_code == 200 assert get_message("WEBAUTHN_REGISTER_SUCCESSFUL", name="testr3") in response.data response = clients.get("/wan-register") assert b"testr3" in response.data # Make sure GET works - this is important if we get a freshness redirect when # attempting to delete - the verify endpoint will redirect back to here. response = clients.get("/wan-delete", follow_redirects=False) assert response.status_code == 302 """ response = clients.post("/wan-delete") assert get_message("WEBAUTHN_NAME_REQUIRED") in response.data """ response = clients.post( "/wan-delete", data=dict(name="testr1"), follow_redirects=True ) assert get_message("WEBAUTHN_NAME_NOT_FOUND", name="testr1") in response.data with capture_flashes() as flashes: response = clients.post( "/wan-delete", data=dict(name="testr3"), follow_redirects=True ) assert flashes[0]["category"] == "info" assert flashes[0]["message"].encode("utf-8") == get_message( "WEBAUTHN_CREDENTIAL_DELETED", name="testr3" ) response = clients.get("/wan-register") assert b"testr3" not in response.data def test_delete_json(app, clients, get_message): headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(clients) register_options, response_url = _register_start_json(clients, name="testr3") response = clients.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 response = clients.get("/wan-register", headers=headers) active_creds = response.json["response"]["registered_credentials"] assert active_creds[0]["name"] == "testr3" response = clients.post("/wan-delete", json=dict()) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf=8") == get_message( "WEBAUTHN_NAME_REQUIRED" ) response = clients.post("/wan-delete", json=dict(name="testr1")) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf=8") == get_message( "WEBAUTHN_NAME_NOT_FOUND", name="testr1" ) response = clients.post("/wan-delete", json=dict(name="testr3")) assert response.status_code == 200 def test_disabled_account(app, client, get_message): # With USER_HINTS enabled, should get 200 on initial signin POST, but # not receive a list of registered credentials. authenticate(client) register_options, response_url = _register_start_json( client, name="testr3", usage="first" ) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.deactivate_user(user) app.security.datastore.commit() signin_options, response_url = _signin_start(client, "matt@lp.com") assert response.status_code == 200 allow_credentials = signin_options["allowCredentials"] assert len(allow_credentials) == 0 # Now set USER_HINTS false and should get 400 on second POST app.config["SECURITY_WAN_ALLOW_USER_HINTS"] = False # Identity should be ignored signin_options, response_url = _signin_start(client, "matt@lp.com") allow_credentials = signin_options["allowCredentials"] assert len(allow_credentials) == 0 response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1)), ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "DISABLED_ACCOUNT" ) def test_unk_credid(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json( client, name="testr3", usage="first" ) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") assert len(signin_options["allowCredentials"]) == 1 bad_signin = copy.deepcopy(SIGNIN_DATA1) bad_signin["rawId"] = bad_signin["rawId"].replace("w", "d") response = client.post( response_url, json=dict(credential=json.dumps(bad_signin)), ) assert response.status_code == 400 assert response.json["response"]["field_errors"]["credential"][0].encode( "utf-8" ) == get_message("WEBAUTHN_UNKNOWN_CREDENTIAL_ID") # same with forms with capture_flashes() as flashes: response = client.post( response_url, data=dict(credential=json.dumps(bad_signin)), follow_redirects=False, ) assert response.status_code == 302 assert "/wan-signin" in response.location assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "WEBAUTHN_UNKNOWN_CREDENTIAL_ID" ) @pytest.mark.settings(wan_allow_as_first_factor=False) def test_no_first_factor(app, client, get_message): # make sure that is app not configured to allow a webauthn key as a 'first' # authenticator, that the endpoint 'disappears'. authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) response = client.post("wan-signin", json=dict(identity="matt@lp.com")) assert response.status_code == 404 @pytest.mark.two_factor() @pytest.mark.unified_signin() def test_tf(app, client, get_message): # Test using webauthn key as a second factor # Register 2 keys - one "first" one "secondary" keys = reg_2_keys(client) logout(client) # log back in - should require MFA. response = client.post( "/us-signin", data=dict(identity="matt@lp.com", passcode="password", remember=True), follow_redirects=True, ) assert response.status_code == 200 assert b"Use a Passkey as a Second Factor" in response.data # we should have a wan key available assert b'action="/wan-signin' in response.data # verify NOT logged in response = client.get("/profile", follow_redirects=False) assert "/login" in response.location signin_options, response_url = _signin_start(client, "matt@lp.com") assert len(signin_options["allowCredentials"]) == 1 assert signin_options["allowCredentials"][0]["id"] == keys["secondary"]["id"] response = client.post( response_url, json=dict(credential=json.dumps(keys["secondary"]["signin"])), ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.two_factor() @pytest.mark.unified_signin() def test_tf_json(app, client, get_message): # Test using webauthn key as a second factor # Register 2 keys - one "first" one "secondary" keys = reg_2_keys(client) logout(client) # log back in - should require MFA. response = client.post( "/us-signin", json=dict(identity="matt@lp.com", passcode="password", remember=True), ) assert response.status_code == 200 assert response.json["response"]["tf_method"] == "webauthn" assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" assert response.json["response"]["tf_signin_url"] == "/wan-signin" # verify NOT logged in response = client.get("/profile", headers={"accept": "application/json"}) assert response.status_code == 401 # For secondary, identity is stored in session signin_options, response_url, _ = _signin_start_json(client, "") assert len(signin_options["allowCredentials"]) == 1 assert signin_options["allowCredentials"][0]["id"] == keys["secondary"]["id"] response = client.post( response_url, json=dict(credential=json.dumps(keys["secondary"]["signin"])), ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", headers={"accept": "application/json"}) assert response.status_code == 200 @pytest.mark.two_factor() @pytest.mark.settings(two_factor_always_validate=False) def test_tf_validity_window(app, client, get_message): # Test with a two-factor validity setting - we don't get re-prompted. authenticate(client) assert not client.get_cookie("tf_validity") register_options, response_url = _register_start_json(client) client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) logout(client) # login - should require second factor response = client.post( "/login", data=dict(email="matt@lp.com", password="password"), follow_redirects=True, ) assert b"Use a Passkey as a Second Factor" in response.data with client.session_transaction() as session: assert "tf_user_id" in session signin_options, response_url = _signin_start(client, "matt@lp.com") response = client.post(response_url, json=dict(credential=json.dumps(SIGNIN_DATA1))) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 reset_signcount(app, "matt@lp.com", "testr1") logout(client) # since we didn't specify 'remember' previously - should still require 2FA response = client.post( "/login", data=dict(email="matt@lp.com", password="password", remember=True), follow_redirects=True, ) assert b"Use a Passkey as a Second Factor" in response.data signin_options, response_url = _signin_start(client, "matt@lp.com") response = client.post(response_url, json=dict(credential=json.dumps(SIGNIN_DATA1))) assert response.status_code == 200 assert client.get_cookie("tf_validity") logout(client) # since we did specify 'remember' previously - should not require 2FA response = client.post( "/login", data=dict(email="matt@lp.com", password="password"), follow_redirects=True, ) # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # Since logged in all tf related attributes in session should be gone. assert not tf_in_session(get_existing_session(client)) @pytest.mark.two_factor() @pytest.mark.settings(two_factor_always_validate=False) def test_tf_validity_window_json(app, client, get_message): # Test with a two-factor validity setting - we don't get re-prompted. # This also relies on the tf_validity_cookie json_authenticate(client) register_options, response_url = _register_start_json(client) client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) logout(client) response = client.post( "/login", json=dict(email="matt@lp.com", password="password", remember=True) ) assert response.status_code == 200 assert response.json["response"]["tf_required"] signin_options, response_url = _signin_start(client, "matt@lp.com") response = client.post(response_url, json=dict(credential=json.dumps(SIGNIN_DATA1))) assert response.status_code == 200 logout(client) # Sign in again - shouldn't require 2FA response = client.post( "/login", json=dict( email="matt@lp.com", password="password", remember=True, ), ) assert response.status_code == 200 response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(wan_register_within="1 seconds") def test_register_timeout(app, client, get_message): authenticate(client) app.security.wan_serializer = FakeSerializer(1.0) register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_EXPIRED", within=app.config["SECURITY_WAN_REGISTER_WITHIN"] ) @pytest.mark.settings(wan_signin_within="2 seconds") def test_signin_timeout(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) app.security.wan_serializer = FakeSerializer(2.0) signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1)), ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_EXPIRED", within=app.config["SECURITY_WAN_SIGNIN_WITHIN"] ) def test_bad_token(app, client, get_message): authenticate(client) response = client.post("/wan-register/not a token", json=dict()) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # same w/o json response = client.post( "/wan-register/not a token", data=dict(), follow_redirects=True ) assert get_message("API_ERROR") in response.data response = client.post( "/wan-register/not a token", data=dict(), follow_redirects=False ) assert "/wan-register" in response.location # Test wan-verify response = client.post("/wan-verify/not a token", json=dict()) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # same w/o json response = client.post( "/wan-verify/not a token", data=dict(), follow_redirects=True ) assert get_message("API_ERROR") in response.data response = client.post( "/wan-verify/not a token", data=dict(), follow_redirects=False ) assert "/wan-verify" in response.location # Test signin logout(client) response = client.post("/wan-signin/not a token", json=dict()) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # same w/o json response = client.post( "/wan-signin/not a token", data=dict(), follow_redirects=True ) assert get_message("API_ERROR") in response.data response = client.post( "/wan-signin/not a token", data=dict(), follow_redirects=False ) assert "/wan-signin" in response.location @pytest.mark.settings( wan_register_template="custom_security/wan_register.html", wan_signin_template="custom_security/wan_signin.html", wan_verify_template="custom_security/wan_verify.html", ) def test_wan_context_processors(client, app): @app.security.context_processor def default_ctx_processor(): return {"global": "global"} @app.security.wan_register_context_processor def register_ctx(): return {"foo": "register"} authenticate(client) response = client.get("wan-register") assert b"CUSTOM WAN REGISTER" in response.data assert b"global" in response.data assert b"register" in response.data response = client.post("wan-register", data=dict(name="matt@lp.com")) assert b"CUSTOM WAN REGISTER" in response.data assert b"global" in response.data assert b"register" in response.data logout(client) @app.security.wan_signin_context_processor def signin_ctx(): return {"foo": "signin"} response = client.get("wan-signin") assert b"CUSTOM WAN SIGNIN" in response.data assert b"global" in response.data assert b"signin" in response.data response = client.post("wan-signin", data=dict(name="matt@lp.com")) assert b"CUSTOM WAN SIGNIN" in response.data assert b"global" in response.data assert b"signin" in response.data @app.security.wan_verify_context_processor def verify_ctx(): return {"foo": "verify"} authenticate(client) response = client.get("wan-verify") assert b"CUSTOM WAN VERIFY" in response.data assert b"global" in response.data assert b"verify" in response.data response = client.post("wan-verify", data=dict(name="matt@lp.com")) assert b"CUSTOM WAN VERIFY" in response.data assert b"global" in response.data assert b"verify" in response.data @pytest.mark.two_factor() def test_alt_tf(app, client, get_message): # Use webauthn as primary and set up SMS as second factor authenticate(client) register_options, response_url = _register_start_json(client, usage="first") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 sms_sender = setup_tf_sms(client) logout(client) # sign in using webauthn key signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1)), ) assert response.status_code == 200 assert response.json["response"]["tf_required"] code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", json=dict(code=code)) assert response.status_code == 200 # verify logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.two_factor() def test_all_in_one(app, client, get_message): # Use a key that supports user_verification - we should be able to # use that alone. authenticate(client) register_options, response_url = _register_start_json(client, usage="first") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA_UV))) assert response.status_code == 200 setup_tf_sms(client) logout(client) signin_options, response_url, rjson = _signin_start_json(client, "matt@lp.com") assert "user" not in rjson["response"] response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA_UV)) ) assert response.json["response"]["user"]["email"] == "matt@lp.com" # verify actually logged in response = client.get("/profile", headers={"accept": "application/json"}) assert response.status_code == 200 @pytest.mark.two_factor() def test_all_in_one_not_allowed(app, client, get_message): # now test when we don't allow a key to satisfy both factors authenticate(client) register_options, response_url = _register_start_json(client, usage="first") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA_UV))) assert response.status_code == 200 setup_tf_sms(client) logout(client) app.config["SECURITY_WAN_ALLOW_AS_MULTI_FACTOR"] = False signin_options, response_url, rjson = _signin_start_json(client, "matt@lp.com") assert "user" not in rjson["response"] response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA_UV)) ) assert response.json["response"]["tf_required"] def test_reset(app, client): headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client) register_options, response_url = _register_start_json(client) client.post(response_url, json=dict(credential=json.dumps(REG_DATA_UV))) response = client.get("/wan-register", headers=headers) active_creds = response.json["response"]["registered_credentials"] assert active_creds[0]["name"] == "testr1" with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.webauthn_reset(user) app.security.datastore.commit() response = client.get("/wan-register", headers=headers) active_creds = response.json["response"]["registered_credentials"] assert len(active_creds) == 0 def test_user_handle(app, clients, get_message): """Test that we fail signin if user_handle doesn't match. Since we generated the SIGNIN_DATA_OH from view_scaffold - the user_handle has no way of matching. """ authenticate(clients) register_options, response_url = _register_start_json(clients, usage="first") response = clients.post(response_url, json=dict(credential=json.dumps(REG_DATA_UH))) assert response.status_code == 200 # verify can't sign in logout(clients) signin_options, response_url, _ = _signin_start_json(clients, "matt@lp.com") response = clients.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA_UH)) ) assert response.json["response"]["field_errors"]["credential"][0].encode( "utf-8" ) == get_message("WEBAUTHN_MISMATCH_USER_HANDLE") # Now change the user_handle both for the user and SIGNIN_DATA_UH with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.set_webauthn_user_handle(user) app.security.datastore.commit() b64_user_handle = urlsafe_b64encode( user.fs_webauthn_user_handle.encode() ).decode("utf-8") upd_signin_data = copy.deepcopy(SIGNIN_DATA_UH) upd_signin_data["response"]["userHandle"] = b64_user_handle signin_options, response_url, _ = _signin_start_json(clients, "matt@lp.com") response = clients.post( response_url, json=dict(credential=json.dumps(upd_signin_data)) ) # verify actually logged in response = clients.get("/profile", headers={"accept": "application/json"}) assert response.status_code == 200 def test_autogen_user_handle(app, client, get_message): # Test that is an existing user doesn't have a fs_webauthn_user_handle - it will # be generated. with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") user.fs_webauthn_user_handle = None app.security.datastore.put(user) app.security.datastore.commit() authenticate(client) register_options, response_url = _register_start_json(client, usage="first") with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") assert user.fs_webauthn_user_handle b64_user_handle = ( urlsafe_b64encode(user.fs_webauthn_user_handle.encode()) .decode("utf-8") .replace("=", "") ) assert b64_user_handle == register_options["user"]["id"] def test_verify_json(app, client, get_message): # Test can re-authenticate using existing webauthn key. headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client) register_options, response_url = _register_start_json(client, usage="first") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get("fresh", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] assert response.json["response"]["has_webauthn_verify_credential"] response = client.get("wan-verify", headers=headers) assert response.status_code == 200 response = client.post("wan-verify", json=dict()) # default webauthn_utils will set userVerification to discouraged in the # case of verify. signin_options = response.json["response"]["credential_options"] assert signin_options["userVerification"] == "discouraged" response_url = ( f'wan-verify/{response.json["response"]["wan_state"]}?include_auth_token' ) response = client.post(response_url, json=dict(credential=json.dumps(SIGNIN_DATA1))) assert response.status_code == 200 verify_token(client, response.json["response"]["user"]["authentication_token"]) response = client.get("fresh", headers=headers) assert response.status_code == 200 def test_verify(app, client, get_message): # Test can re-authenticate using existing webauthn key. # Forms version - verify that the 'next' qparam is properly maintained during the # 2 part authentication. authenticate(client) register_options, response_url = _register_start(client, usage="first") response = client.post( response_url, data=dict(credential=json.dumps(REG_DATA1)), follow_redirects=True ) assert response.status_code == 200 assert b"testr1" in response.data old_paa = reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get("fresh") assert response.location == "/verify?next=/fresh" signin_options, response_url = _signin_start( client, endpoint="wan-verify?next=/fresh" ) response = client.post( response_url, data=dict(credential=json.dumps(SIGNIN_DATA1)), follow_redirects=False, ) assert "/fresh" in response.location with client.session_transaction() as sess: assert sess["fs_paa"] > old_paa @pytest.mark.settings(wan_signin_within="2 seconds") def test_verify_timeout(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 app.security.wan_serializer = FakeSerializer(2.0) response = client.post("wan-verify", json=dict()) response_url = f'wan-verify/{response.json["response"]["wan_state"]}' response = client.post(response_url, json=dict(credential=json.dumps(SIGNIN_DATA1))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_EXPIRED", within=app.config["SECURITY_WAN_SIGNIN_WITHIN"] ) def test_verify_validate_error(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 response = client.post("wan-verify", json=dict()) response_url = f'wan-verify/{response.json["response"]["wan_state"]}' # send wrong signin data response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA_UH)) ) assert response.status_code == 400 assert response.json["response"]["field_errors"]["credential"][0].encode( "utf-8" ) == get_message("WEBAUTHN_UNKNOWN_CREDENTIAL_ID") # same thing - with forms - this should redirect to wan-verify and flash a message with capture_flashes() as flashes: response = client.post( response_url, data=dict(credential=json.dumps(SIGNIN_DATA_UH)), follow_redirects=False, ) assert response.status_code == 302 assert "/wan-verify" in response.location assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "WEBAUTHN_UNKNOWN_CREDENTIAL_ID" ) @pytest.mark.settings(wan_allow_as_verify=None) def test_no_verify(app, client): authenticate(client) response = client.get("/wan-verify") assert response.status_code == 404 def test_verify_usage_any_json(app, client, get_message): # Test the WAN_ALLOW_AS_VERIFY config. # Make sure only allowed credentials show up as options # Make sure if we use a disallowed credential, we get an error. keys = reg_2_keys(client) # Default WAN_ALLOW_AS_VERIFY is ["first", "secondary"] response = client.post("wan-verify", json=dict()) response_url = f'wan-verify/{response.json["response"]["wan_state"]}' allow_credentials = response.json["response"]["credential_options"][ "allowCredentials" ] assert len(allow_credentials) == 2 # make sure can sign in with either response = client.post( response_url, json=dict(credential=json.dumps(keys["first"]["signin"])) ) assert response.status_code == 200 response = client.post( response_url, json=dict(credential=json.dumps(keys["secondary"]["signin"])) ) assert response.status_code == 200 @pytest.mark.settings(wan_allow_as_verify="first") def test_verify_usage_first_json(app, client, get_message): # Test the WAN_ALLOW_AS_VERIFY config. # Make sure only allowed credentials show up as options # Make sure if we use a disallowed credential, we get an error. keys = reg_2_keys(client) response = client.post("wan-verify", json=dict()) response_url = f'wan-verify/{response.json["response"]["wan_state"]}' allow_credentials = response.json["response"]["credential_options"][ "allowCredentials" ] assert len(allow_credentials) == 1 assert allow_credentials[0]["id"] == keys["first"]["id"] # make sure can sign in with just "first" response = client.post( response_url, json=dict(credential=json.dumps(keys["first"]["signin"])) ) assert response.status_code == 200 # but not "secondary" response = client.post( response_url, json=dict(credential=json.dumps(keys["secondary"]["signin"])) ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_CREDENTIAL_WRONG_USAGE" ) @pytest.mark.settings(wan_allow_as_verify="secondary") def test_verify_usage_secondary_json(app, client, get_message): # Test the WAN_ALLOW_AS_VERIFY config. # Make sure only allowed credentials show up as options # Make sure if we use a disallowed credential, we get an error. keys = reg_2_keys(client) response = client.post("wan-verify", json=dict()) response_url = f'wan-verify/{response.json["response"]["wan_state"]}' allow_credentials = response.json["response"]["credential_options"][ "allowCredentials" ] assert len(allow_credentials) == 1 assert allow_credentials[0]["id"] == keys["secondary"]["id"] # make sure can sign in with just "secondary" response = client.post( response_url, json=dict(credential=json.dumps(keys["first"]["signin"])) ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_CREDENTIAL_WRONG_USAGE" ) response = client.post( response_url, json=dict(credential=json.dumps(keys["secondary"]["signin"])) ) assert response.status_code == 200 def test_remember_token(client): # test that remember token properly set on primary authn with webauthn authenticate(client) register_options, response_url = _register_start_json( client, name="testr3", usage="first" ) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) assert not client.get_cookie("remember_token") headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.post( "wan-signin", headers=headers, json=dict(identity="matt@lp.com", remember=True) ) response_url = f'wan-signin/{response.json["response"]["wan_state"]}' assert response.json["response"]["remember"] response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1), remember=True), ) assert client.get_cookie("remember_token") client.delete_cookie("session") response = client.get("/profile") assert b"profile" in response.data @pytest.mark.two_factor() @pytest.mark.unified_signin() def test_remember_token_tf(client): # test that remember token properly set after secondary authn with webauthn authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) assert not client.get_cookie("remember_token") # login again - should require MFA response = client.post( "/us-signin", json=dict(identity="matt@lp.com", passcode="password", remember=True), ) assert response.status_code == 200 assert response.json["response"]["tf_method"] == "webauthn" assert response.json["response"]["tf_required"] with client.session_transaction() as session: assert session["tf_remember_login"] signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1), remember=True), ) assert client.get_cookie("remember_token") client.delete_cookie("session") response = client.get("/profile") assert b"profile" in response.data @pytest.mark.settings( wan_post_register_view="/post_register", ) def test_post_register_redirect(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") response = client.post( response_url, data=dict(credential=json.dumps(REG_DATA1)), follow_redirects=False, ) assert response.status_code == 302 assert "/post_register" in response.location class MyWebauthnUtil(HackWebauthnUtil): def user_verification(self, user, usage): from webauthn.helpers.structs import UserVerificationRequirement return UserVerificationRequirement.REQUIRED @pytest.mark.two_factor() @pytest.mark.unified_signin() @pytest.mark.webauthn(webauthn_util_cls=MyWebauthnUtil) def test_uv_required(client): # Override WebauthnUtils to require user-verification on signin. keys = reg_2_keys(client) logout(client) # log back in - should require MFA. response = client.post( "/us-signin", json=dict(identity="matt@lp.com", passcode="password", remember=True), ) assert response.status_code == 200 assert response.json["response"]["tf_required"] # since we always REQUIRE user_verification in our WebauthUtil this should fail signin_options, response_url, _ = _signin_start_json(client, "") response = client.post( response_url, json=dict(credential=json.dumps(keys["secondary"]["signin"])), ) assert response.status_code == 400 assert ( "User verification is required" in response.json["response"]["field_errors"]["credential"][0] ) logout(client) # Try signing in with 'first' WebAuthn key - this DOES have UV set so should work. signin_options, response_url, _ = _signin_start_json(client, "") response = client.post( response_url, json=dict(credential=json.dumps(keys["first"]["signin"])), ) assert response.status_code == 200 assert response.json["response"]["user"]["email"] == "matt@lp.com" @pytest.mark.settings(multi_factor_recovery_codes=True) def test_mf(client): # Test using recovery codes in-liu of a webauthn second factor # Note that we are allowed to generate recovery codes even if we don't yet have # an established 2nd factor headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client) response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 # setup webauthn register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) response = client.post( "/login", json=dict(email="matt@lp.com", password="password") ) assert response.status_code == 200 assert response.json["response"]["tf_required"] # oh no - we forgot our webauthn key """ Right now tf-rescue is part of TWO_FACTOR - not WEBAUTHN response = client.get("/tf-rescue", headers=headers) options = response.json["response"]["recovery_options"] assert "recovery_code" in options.keys() assert "/mf-recovery" in options["recovery_code"] """ response = client.post( "/mf-recovery", data=dict(code=codes[0]), follow_redirects=True ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(url_prefix="/auth") def test_login_next(app, client, get_message): # Test that ?next=/xx is propagated through login/wan-signin templates as well as # views. # Also - use a different blueprint prefix - we rarely test that.... authenticate(client, endpoint="/auth/login") register_options, response_url = _register_start( client, name="testr3", usage="first", endpoint="/auth/wan-register" ) response = client.post( response_url, data=dict(credential=json.dumps(REG_DATA1)), follow_redirects=True ) assert response.status_code == 200 assert get_message("WEBAUTHN_REGISTER_SUCCESSFUL", name="testr3") in response.data logout(client, endpoint="/auth/logout") response = client.get("profile", follow_redirects=True) assert "?next=/profile" in response.request.url # pull webauthn form action out of login_form - should have ?next=... webauthn_url = get_form_action(response, 1) signin_options, response_url = _signin_start( client, "matt@lp.com", endpoint=webauthn_url ) response = client.post( response_url, data=dict(credential=json.dumps(SIGNIN_DATA1)), follow_redirects=True, ) assert response.status_code == 200 assert b"Profile Page" in response.data # Try form.next logout(client, endpoint="/auth/logout") reset_signcount(app, "matt@lp.com", "testr3") response = client.post( "/auth/wan-signin", data=dict(identity="matt@lp.com", next="/im-in") ) response_url = get_form_action(response) next_loc = get_form_input_value(response, "next") response = client.post( response_url, data=dict(credential=json.dumps(SIGNIN_DATA1), next=next_loc), follow_redirects=False, ) assert "/im-in" in response.location @pytest.mark.flask_async() def test_async(app, client, get_message): auths = [] @user_authenticated.connect_via(app) async def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) @wan_registered.connect_via(app) async def pc(sender, user, name, **extra_args): assert name == "testr1" assert len(user.webauthn) == 1 @wan_deleted.connect_via(app) async def wan_delete(sender, user, name, **extra_args): assert name == "testr1" authenticate(client) register_options, response_url = _register_start(client, usage="first") response = client.post( response_url, data=dict(credential=json.dumps(REG_DATA1)), follow_redirects=True ) assert response.status_code == 200 # sign in - simple case use identity so we get back allowCredentials logout(client) signin_options, response_url = _signin_start(client, "matt@lp.com") response = client.post( response_url, data=dict(credential=json.dumps(SIGNIN_DATA1)), follow_redirects=True, ) assert response.status_code == 200 assert b"Welcome matt@lp.com" in response.data assert len(auths) == 2 assert auths[1][1] == ["webauthn"] # test delete signal response = client.post( "/wan-delete", data=dict(name="testr1"), follow_redirects=True ) @pytest.mark.csrf() @pytest.mark.settings( wan_post_register_view="/done-registration", post_login_view="/post-login", ) def test_csrf(app, client, get_message): response = client.get("/login") csrf_token = get_form_input_value(response, "csrf_token") authenticate(client, csrf=True) register_options, response_url = _register_start( client, usage="first", csrf_token=csrf_token ) data = dict(credential=json.dumps(REG_DATA1)) response = client.post(response_url, data=data, follow_redirects=True) assert ( b"The CSRF token is missing" in response.data ) # this should have been flashed data["csrf_token"] = csrf_token response = client.post(response_url, data=data) assert check_location(app, response.location, "/done-registration") logout(client) # use old csrf_token - should fail and we should get the error in the template response = client.post( "wan-signin", data=dict(identity="matt@lp.com", csrf_token=csrf_token) ) assert b"The CSRF tokens do not match." in response.data response = client.get("/wan-signin") csrf_token = get_form_input_value(response, "csrf_token") signin_options, response_url = _signin_start( client, "matt@lp.com", csrf_token=csrf_token ) data = dict(credential=json.dumps(SIGNIN_DATA1)) response = client.post(response_url, data=data, follow_redirects=True) assert ( b"The CSRF token is missing" in response.data ) # this should have been flashed data["csrf_token"] = csrf_token response = client.post(response_url, data=data) assert check_location(app, response.location, "/post-login") flask-security-5.7.1/tests/translations/000077500000000000000000000000001511046741400203575ustar00rootroot00000000000000flask-security-5.7.1/tests/translations/fr_FR/000077500000000000000000000000001511046741400213555ustar00rootroot00000000000000flask-security-5.7.1/tests/translations/fr_FR/LC_MESSAGES/000077500000000000000000000000001511046741400231425ustar00rootroot00000000000000flask-security-5.7.1/tests/translations/fr_FR/LC_MESSAGES/flask_security.mo000066400000000000000000000007631511046741400265340ustar00rootroot00000000000000,<=OPassword no-workyProject-Id-Version: PROJECT VERSION Report-Msgid-Bugs-To: EMAIL@ADDRESS POT-Creation-Date: 2021-03-30 08:52-0700 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language: fr_FR Language-Team: fr_FR Plural-Forms: nplurals=2; plural=(n > 1) MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Generated-By: Babel 2.9.0 Passe - no-workyflask-security-5.7.1/tests/translations/fr_FR/LC_MESSAGES/flask_security.po000066400000000000000000000001101511046741400265210ustar00rootroot00000000000000msgid "" msgstr "" msgid "Password no-worky" msgstr "Passe - no-worky" flask-security-5.7.1/tests/view_scaffold.py000066400000000000000000000302641511046741400210300ustar00rootroot00000000000000# :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). # :license: MIT, see LICENSE for more details. """ This is a simple scaffold that can be run as an app and manually test various views using a browser. It can be used to test translations by adding ?lang=xx. You might need to delete the session cookie if you need to switch between languages (it is easy to do this with your browser development tools). Configurations can be set via environment variables. Runs on port 5001 An initial user: test@test.com/password is created. If you want to register a new user - you will receive a 'flash' that has the confirm URL (with token) you need to enter into your browser address bar. Since we don't actually send email - we have signal handlers flash the required data and a mail sender that flashes what mail would be sent! """ from __future__ import annotations import base64 import secrets from datetime import timedelta import os import typing as t import webbrowser from flask import Flask, flash, render_template_string, request, session, g from flask_wtf import CSRFProtect from flask_security import ( MailUtil, Security, UserDatastore, UserMixin, WebauthnUtil, auth_required, current_user, SQLAlchemyUserDatastore, FSQLALiteUserDatastore, ) from flask_security.signals import ( us_security_token_sent, tf_security_token_sent, reset_password_instructions_sent, user_not_registered, user_registered, ) from flask_security.utils import ( hash_password, naive_utcnow, uia_email_mapper, uia_phone_mapper, ) def _find_bool(v): if str(v).lower() in ["true"]: return True elif str(v).lower() in ["false"]: return False return v class FlashMailUtil(MailUtil): def send_mail( self, template: str, subject: str, recipient: str, sender: str | tuple, body: str, html: str | None, **kwargs: t.Any, ) -> None: flash(f"Email body: {body}") if html: hb = html.encode() url = "data:text/html;base64," + base64.b64encode(hb).decode() webbrowser.open(url, new=1) SET_LANG = False def fsqla_datastore(app): from flask_sqlalchemy import SQLAlchemy from flask_security.models import fsqla_v3 as fsqla from sqlalchemy_utils import database_exists, create_database # Create database models and hook up. app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config.setdefault("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): pass class WebAuthn(db.Model, fsqla.FsWebAuthnMixin): pass with app.app_context(): if not database_exists(db.engine.url): create_database(db.engine.url) db.create_all() return SQLAlchemyUserDatastore(db, User, Role, WebAuthn) def fsqla_lite_datastore(app: Flask) -> FSQLALiteUserDatastore: from sqlalchemy.orm import DeclarativeBase from flask_sqlalchemy_lite import SQLAlchemy from flask_security.models import sqla as sqla from sqlalchemy_utils import database_exists, create_database # Create database models and hook up. app.config.setdefault("SQLALCHEMY_DATABASE_URI", "sqlite:///:memory:") app.config |= { "SQLALCHEMY_ENGINES": { "default": { "url": app.config["SQLALCHEMY_DATABASE_URI"], "pool_pre_ping": True, }, }, } db = SQLAlchemy(app) class Model(DeclarativeBase): pass sqla.FsModels.set_db_info(base_model=Model) class Role(Model, sqla.FsRoleMixin): __tablename__ = "role" pass class User(Model, sqla.FsUserMixin): __tablename__ = "user" pass class WebAuthn(Model, sqla.FsWebAuthnMixin): __tablename__ = "web_authn" # N.B. this is name that Flask-SQLAlchemy gives. pass with app.app_context(): if not database_exists(db.engine.url): create_database(db.engine.url) Model.metadata.create_all(db.engine) return FSQLALiteUserDatastore(db, User, Role, WebAuthn) def create_app() -> Flask: # Use real templates - not test templates... app = Flask("view_scaffold", template_folder="../") app.config["DEBUG"] = True # SECRET_KEY generated using: secrets.token_urlsafe() app.config["SECRET_KEY"] = "pf9Wkove4IKEAXvy-cQkeDPhv9Cb3Ag-wyJILbq_dFw" # PASSWORD_SALT secrets.SystemRandom().getrandbits(128) app.config["SECURITY_PASSWORD_SALT"] = "156043940537155509276282232127182067465" app.config["LOGIN_DISABLED"] = False app.config["WTF_CSRF_ENABLED"] = True app.config["REMEMBER_COOKIE_SAMESITE"] = "strict" # 'strict' causes redirect after oauth to fail since session cookie not sent # this just happens on first 'register' with e.g. github # app.config["SESSION_COOKIE_SAMESITE"] = "strict" app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"] = [ {"email": {"mapper": uia_email_mapper, "case_insensitive": True}}, {"us_phone_number": {"mapper": uia_phone_mapper}}, ] # app.config["SECURITY_US_ENABLED_METHODS"] = ["password"] # app.config["SECURITY_US_ENABLED_METHODS"] = ["authenticator", "password"] # app.config["SECURITY_US_SIGNIN_REPLACES_LOGIN"] = True # app.config["SECURITY_WAN_ALLOW_USER_HINTS"] = False # Setup script nonces and test with nonce-based content-security policy app.config["SECURITY_SCRIPT_NONCE_KEY"] = "csp_nonce" @app.before_request def set_nonce(): g.csp_nonce = secrets.token_urlsafe(16) @app.after_request def inject_csp_header(response): response.headers["Content-Security-Policy"] = ( f"script-src 'nonce-{g.csp_nonce}'" ) return response app.config["SECURITY_TOTP_SECRETS"] = { "1": "TjQ9Qa31VOrfEzuPy4VHQWPCTmRzCnFzMKLxXYiZu9B" } app.config["SECURITY_TOTP_ISSUER"] = "me" app.config["SECURITY_FRESHNESS"] = timedelta(minutes=10) app.config["SECURITY_FRESHNESS_GRACE_PERIOD"] = timedelta(minutes=20) app.config["SECURITY_USERNAME_ENABLE"] = True app.config["SECURITY_USERNAME_REQUIRED"] = True app.config["SECURITY_PASSWORD_REQUIRED"] = False # allow registration w/o password app.config["SECURITY_RETURN_GENERIC_RESPONSES"] = False # enable oauth - note that this assumes that app is passes XXX_CLIENT_ID and # XXX_CLIENT_SECRET as environment variables. app.config["SECURITY_OAUTH_ENABLE"] = True # app.config["SECURITY_URL_PREFIX"] = "/fs" class TestWebauthnUtil(WebauthnUtil): def generate_challenge(self, nbytes: int | None = None) -> str: # Use a constant Challenge so we can use this app to generate gold # responses for use in unit testing. See test_webauthn. # NEVER NEVER NEVER do this in production return "smCCiy_k2CqQydSQ_kPEjV5a2d0ApfatcpQ1aXDmQPo" def origin(self) -> str: # Return the RP origin - normally this is just the URL of the application. # To test with ngrok - we need the https address that the browser originally # sent - it is sent as the ORIGIN header - not sure if this should be # default or just for testing. return request.origin if request.origin else request.host_url.rstrip("/") # Turn on all features (except passwordless since that removes normal login) for opt in [ "changeable", "change_email", "change_username", "recoverable", "registerable", "trackable", "NOTpasswordless", "confirmable", "two_factor", "username_recovery", "unified_signin", "webauthn", "multi_factor_recovery_codes", ]: app.config["SECURITY_" + opt.upper()] = True if os.environ.get("SETTINGS"): # Load settings from a file pointed to by SETTINGS app.config.from_envvar("SETTINGS") # Allow any SECURITY_, SQLALCHEMY, Authlib config to be set in environment. for ev in os.environ: if ( ev.startswith("SECURITY_") or ev.startswith("SQLALCHEMY_") or "_CLIENT_" in ev ): app.config[ev] = _find_bool(os.environ.get(ev)) CSRFProtect(app) # Setup Flask-Security # user_datastore = fsqla_datastore(app) user_datastore = fsqla_lite_datastore(app) security = Security( app, user_datastore, webauthn_util_cls=TestWebauthnUtil, mail_util_cls=FlashMailUtil, ) # Setup Babel def get_locale(): # For a given session - set lang based on first request. # Honor explicit url request first if not session: # if running CLI return global SET_LANG if not SET_LANG: session.pop("lang", None) SET_LANG = True if "lang" not in session: locale = request.args.get("lang", None) if not locale: locale = request.accept_languages.best if not locale: locale = "en" if locale: session["lang"] = locale return session.get("lang", None).replace("-", "_") try: import flask_babel flask_babel.Babel(app, locale_selector=get_locale) except ImportError: pass @user_registered.connect_via(app) def on_user_registered( myapp: Flask, user: UserMixin, confirm_token: str, **extra: dict[str, t.Any] ) -> None: flash(f"To confirm {user.email} - go to /confirm/{confirm_token}") @user_not_registered.connect_via(app) def on_user_not_registered(myapp, **extra): if extra.get("existing_email"): flash(f"Tried to register existing email: {extra['user'].email}") elif extra.get("existing_username"): flash( f"Tried to register email: {extra['form_data'].email.data}" f" with username: {extra['form_data'].username.data}" ) else: flash("Not registered response - but ??") @reset_password_instructions_sent.connect_via(app) def on_reset(myapp, user, token, **extra): flash(f"Go to /reset/{token}") @tf_security_token_sent.connect_via(app) def on_token_sent(myapp, user, token, method, **extra): flash( "User {} was sent two factor token {} via {}".format( user.calc_username(), token, method ) ) @us_security_token_sent.connect_via(app) def on_us_token_sent(myapp, user, token, method, **extra): flash( "User {} was sent sign in code {} via {}".format( user.calc_username(), token, method ) ) # Views @app.route("/") @auth_required() def home(): return render_template_string( """ {% include 'security/_messages.html' %} {{ _fsdomain('Welcome') }} {{email}} ! {% include "security/_menu.html" %} """, email=current_user.email, security=security, ) @app.route("/basicauth") @auth_required("basic") def basic(): return render_template_string("Basic auth success") @app.route("/protected") @auth_required() def protected(): return render_template_string("Protected endpoint") return app def add_user( ds: UserDatastore, email: str, password: str, role_names: list[str] ) -> None: pw = hash_password(password) roles = [ds.find_or_create_role(rn) for rn in role_names] ds.commit() user = ds.create_user( email=email, password=pw, active=True, confirmed_at=naive_utcnow() ) ds.commit() for role in roles: ds.add_role_to_user(user, role) ds.commit() if __name__ == "__main__": myapp = create_app() security: Security = myapp.extensions["security"] with myapp.app_context(): test_acct = "test@test.com" if not security.datastore.find_user(email=test_acct): add_user(security.datastore, test_acct, "password", ["admin"]) print("Created User: {} with password {}".format(test_acct, "password")) myapp.run(port=5001) flask-security-5.7.1/tox.ini000066400000000000000000000106261511046741400160140ustar00rootroot00000000000000[tox] basepython = python3.12 envlist = py{310,311,312,313,py310}-{low,release,appinstall} mypy async nowebauthn nobabel noauthlib style docs coverage makedist skip_missing_interpreters = false [testenv] allowlist_externals = tox [testenv:pypy310-release] deps = -r requirements/tests.txt commands = tox -e compile_catalog pytest -W ignore --basetemp={envtmpdir} {posargs:tests} [testenv:py{310,311,312,313}-release] deps = -r requirements/tests.txt commands = tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs:tests} [testenv:py{310,311,312,py310}-low] deps = pytest extras = low commands = tox -e compile_catalog pytest -W ignore --basetemp={envtmpdir} {posargs:tests} # manual test to check how we're keeping up with Pallets latest [testenv:main] basepython = python3.12 deps = -r requirements/tests.txt git+https://github.com/pallets/werkzeug@main#egg=werkzeug git+https://github.com/pallets/flask@main#egg=flask git+https://github.com/pallets/flask-sqlalchemy@main#egg=flask-sqlalchemy git+https://github.com/pallets/jinja@main#egg=jinja2 git+https://github.com/pallets-eco/flask-principal@main#egg=flask-principal git+https://github.com/wtforms/wtforms@main#egg=wtforms git+https://github.com/maxcountryman/flask-login@main#egg=flask-login commands = tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs:tests} [testenv:async] deps = -r requirements/tests.txt commands = pip install flask[async] tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs:tests} [testenv:nowebauthn] deps = -r requirements/tests.txt commands = pip uninstall -y webauthn tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs:tests} [testenv:nobabel] deps = -r requirements/tests.txt commands = pip uninstall -y babel flask_babel pytest --basetemp={envtmpdir} {posargs:tests} [testenv:noauthlib] deps = -r requirements/tests.txt commands = pip uninstall -y authlib tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs:tests} [testenv:noflasksqlalchemy] deps = -r requirements/tests.txt commands = pip uninstall -y flask_sqlalchemy flask_sqlalchemy_lite tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs:tests} [testenv:style] deps = pre-commit skip_install = true commands = pre-commit autoupdate pre-commit run --all-files --show-diff-on-failure [testenv:docs] deps = -r requirements/docs.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html [testenv:coverage] deps = -r requirements/tests.txt commands = tox -e compile_catalog coverage run --source=flask_security -m pytest coverage xml [testenv:realpostgres] deps = -r requirements/tests.txt psycopg2-binary commands = # Requires that --realdburl be set otherwise doesn't actually use DB tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs} [testenv:makedist] deps = -r requirements/tests.txt flit twine check-wheel-contents commands = tox -e compile_catalog flit build --no-use-vcs check-wheel-contents dist [testenv:makedist-too] deps = -r requirements/tests.txt flit twine check-wheel-contents commands = tox -e compile_catalog flit -f pyproject-too.toml build --no-use-vcs check-wheel-contents dist [testenv:mypy] deps = -r requirements/tests.txt mypy commands = mypy --install-types --non-interactive flask_security tests [testenv:py{310,311,312,313}-appinstall] commands = python -c "from flask_security import Security; s = Security()" [testenv:compile_catalog] deps = babel skip_install = true commands = pybabel compile --domain flask_security -d flask_security/translations [testenv:extract_messages] deps = babel jinja2 skip_install = true commands = pybabel extract --version 5.7.1 --keyword=_fsdomain --project=Flask-Security \ -o flask_security/translations/flask_security.pot \ --msgid-bugs-address=jwag956@github.com --mapping-file=babel.ini \ --add-comments=NOTE flask_security [testenv:update_catalog] deps = babel skip_install = true commands = pybabel update --domain flask_security -i flask_security/translations/flask_security.pot \ -d flask_security/translations --no-fuzzy-matching [flake8] max-line-length = 88 per-file-ignores = tests/view_scaffold.py: E402