pax_global_header00006660000000000000000000000064145177410440014521gustar00rootroot0000000000000052 comment=88eca62d4f87a36b1339ea8fe5a8b7ad878cf3ef flask-login-0.6.3/000077500000000000000000000000001451774104400137355ustar00rootroot00000000000000flask-login-0.6.3/.editorconfig000066400000000000000000000003311451774104400164070ustar00rootroot00000000000000root = true [*] indent_style = space indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true end_of_line = lf charset = utf-8 max_line_length = 88 [*.{yml,yaml,json,js,css,html}] indent_size = 2 flask-login-0.6.3/.git-blame-ignore-revs000066400000000000000000000004061451774104400200350ustar00rootroot00000000000000# move to src directory 610b99845e667c81e2d422531476f2ea4fecdb04 # apply pyupgrade 6032c9a193170f11ef9c6e0d225c41e9c12d3d4a # apply reorder_python_imports 379166376da287bf4ee5b2e87e25c8ebf1b2bfd1 # apply black formatting 379166376da287bf4ee5b2e87e25c8ebf1b2bfd1 flask-login-0.6.3/.github/000077500000000000000000000000001451774104400152755ustar00rootroot00000000000000flask-login-0.6.3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001451774104400174605ustar00rootroot00000000000000flask-login-0.6.3/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000015021451774104400221500ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. flask-login-0.6.3/.github/dependabot.yml000066400000000000000000000002471451774104400201300ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" day: "monday" time: "16:00" timezone: "UTC" flask-login-0.6.3/.github/workflows/000077500000000000000000000000001451774104400173325ustar00rootroot00000000000000flask-login-0.6.3/.github/workflows/lock.yaml000066400000000000000000000005211451774104400211440ustar00rootroot00000000000000name: 'Lock threads' on: schedule: - cron: '0 0 * * *' jobs: lock: runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@v3 with: github-token: ${{ github.token }} issue-inactive-days: 14 pr-inactive-days: 14 issue-lock-reason: '' pr-lock-reason: '' flask-login-0.6.3/.github/workflows/pre-commit.yaml000066400000000000000000000006321451774104400222730ustar00rootroot00000000000000name: pre-commit on: pull_request: push: branches: [main] jobs: pre-commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.10' cache: 'pip' cache-dependency-path: 'requirements/*.txt' - run: pip install -r requirements/style.txt - uses: pre-commit/action@v3.0.0 flask-login-0.6.3/.github/workflows/publish-release.yml000066400000000000000000000013361451774104400231440ustar00rootroot00000000000000on: push: tags: - "[0-9]+.[0-9]+.[0-9]+" jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.10' cache: 'pip' cache-dependency-path: 'requirements/*.txt' - name: update pip run: | pip install -U wheel pip install -U setuptools python -m pip install -U pip - run: pip install -r requirements/ci-release.txt - run: python -m build - run: twine check dist/* - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@v1.5.0 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} flask-login-0.6.3/.github/workflows/tests.yaml000066400000000000000000000022301451774104400213550ustar00rootroot00000000000000name: Tests on: push: branches: - main pull_request: branches: - main jobs: tests: name: ${{ matrix.name }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - {name: '3.10', python: '3.10', tox: py310} - {name: '3.9', python: '3.9', tox: py39} - {name: '3.8', python: '3.8', tox: py38} - {name: '3.7', python: '3.7', tox: py37} - {name: 'PyPy', python: 'pypy-3.8', tox: pypy38} - {name: 'Minimum Versions', python: '3.9', tox: py-min} steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} cache: 'pip' cache-dependency-path: 'requirements/*.txt' - name: update pip run: | pip install -U wheel pip install -U setuptools python -m pip install -U pip - run: pip install -r requirements/ci-tests.txt - run: tox -e ${{ matrix.tox }} - run: coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} if: ${{ matrix.tox != 'style' }} flask-login-0.6.3/.gitignore000066400000000000000000000002021451774104400157170ustar00rootroot00000000000000.idea/ .vscode/ __pycache__/ *.pyc .pytest_cache/ .tox/ .coverage .coverage.* htmlcov/ docs/_build/ *.egg-info build/ dist/ venv/ flask-login-0.6.3/.pre-commit-config.yaml000066400000000000000000000016301451774104400202160ustar00rootroot00000000000000ci: autoupdate_schedule: monthly repos: - repo: https://github.com/asottile/pyupgrade rev: v3.15.0 hooks: - id: pyupgrade args: ["--py37-plus"] - repo: https://github.com/asottile/reorder_python_imports rev: v3.12.0 hooks: - id: reorder-python-imports additional_dependencies: ["setuptools>60.9"] - repo: https://github.com/psf/black rev: 23.10.1 hooks: - id: black - repo: https://github.com/PyCQA/flake8 rev: 6.1.0 hooks: - id: flake8 additional_dependencies: - flake8-bugbear - flake8-implicit-str-concat - repo: https://github.com/peterdemin/pip-compile-multi rev: v2.6.3 hooks: - id: pip-compile-multi-verify - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: fix-byte-order-marker - id: trailing-whitespace - id: end-of-file-fixer flask-login-0.6.3/.readthedocs.yaml000066400000000000000000000003211451774104400171600ustar00rootroot00000000000000version: 2 build: os: ubuntu-20.04 tools: python: "3.10" python: install: - requirements: requirements/docs.txt - method: pip path: . sphinx: builder: dirhtml fail_on_warning: true flask-login-0.6.3/CHANGES.md000066400000000000000000000155321451774104400153350ustar00rootroot00000000000000Flask-Login Changelog ===================== Version 0.6.3 ------------- Released 2023-10-30 - Compatibility with Flask 3 and Werkzeug 3. #813 Version 0.6.2 ------------- Released on July 25th, 2022 - Fix compatibility with Werkzeug 2.2 and Flask 2.2. #691 - Revert change to `expand_login_view` that attempted to preserve a dynamic subdomain value. Such values should be handled using `app.url_value_preprocessor` and `app.url_defaults`. #691 - Ensure deprecation warnings are present for deprecated features that will be removed in the next feature release. - Use `request_loader` instead of `header_loader`. - Use `user_loaded_from_request` instead of `user_loaded_from_header`. - Use `app.config["LOGIN_DISABLED"]` instead of `_login_disabled`. - Use `init_app` instead of `setup_app`. Version 0.6.1 ------------- Released on May 1st, 2022 - Only preserve subdomain or host view args in unauthorized redirect #663 - The new utility function `login_remembered` returns `True` if the current login is remembered across sessions. #654 - Fix side effect potentially executing view twice for same request. #666 - Clarify usage of FlaskLoginClient test client in docs. #668 Version 0.6.0 ------------- Released on March 30th, 2022 - Drop support for Python 2.7, 3.5, and 3.6, which have all reached the end of their official support. #594, #638 - The minimum supported version of Flask is 1.0.4, and Werkzeug is 1.0.1. However, projects are advised to use the latest versions of both. #639 - Only flash "needs_refresh_message" if value is set #464 - Modify `expand_login_view` to allow for subdomain and host matching for `login_view` #462 - Add accessors for `request_loader` and `user_loader` callback functions #472 - Change "remember_me" cookie to match Werkzeug default value #488 - Change "remember_me" cookie to `HttpOnly`, matching Flask session cookie #488 - Add example for using `unauthorized_handler` #492 - Fix `assertEqual` deprecation warning in pytest #518 - Fix `collections` deprecation warning under Python 3.8 #525 - Replace `safe_str_cmp` with `hmac.compare_digest` #585 - Document `REMEMBER_COOKIE_SAMESITE` config #577 - Revise setup.py to use README.md for long description #598 - Various documentation corrections #484, #482, #487, #534 - Fix `from flask_login import *` behavior, although note that `import *` is not usually a good pattern in code. #485 - `UserMixin.is_authenticated` will return whatever `is_active` returns by default. This prevents inactive users from logging in. #486, #530 - Session protection will only mark the session as not fresh if it's not already marked as such, avoiding modifying the session cookie unnecessarily. #612 Version 0.5.0 ------------- Released on February 9th, 2020 - New custom test client: `flask_login.FlaskLoginClient`. You can use this to write clearer automated tests. #431 - Prefix authenticated user_id, remember, and remember_seconds in Flask Session with underscores to prevent accidental usage in application code. #470 - Simplify user loading. #378 - Various documentation improvements. #393, #394, #397, #417 - Set session ID when setting next. #403 - Clear session identifier on logout. #404 - Ensure use of a safe and up-to-date version of Flask. - Drop support of Python versions: 2.6, 3.3, 3.4 #450 Version 0.4.1 ------------- Released on December 2nd, 2017 - New config option USE_SESSION_FOR_NEXT to enable storing next url in session instead of url. #330 - Accept int seconds along with timedelta for REMEMBER_COOKIE_DURATION. #370 - New config option FORCE_HOST_FOR_REDIRECTS to force host for redirects. #371 Version 0.4.0 ------------- Released on October 26th, 2016 - Fixes OPTIONS exemption from login. #244 - Fixes use of MD5 by replacing with SHA512. #264 - BREAKING: The `login_manager.token_handler` function, `get_auth_token` method on the User class, and the `utils.make_secure_token` utility function have been removed to prevent users from creating insecure auth implementations. Use the `Alternative Tokens` example from the docs instead. #291 Version 0.3.2 ------------- Released on October 8th, 2015 - Fixes Python 2.6 compatibility. - Updates SESSION_KEYS to include "remember". Version 0.3.1 ------------- Released on September 30th, 2015 - Fixes removal of non-Flask-Login keys from session object when using 'strong' protection. Version 0.3.0 ------------- Released on September 10th, 2015 - Fixes handling of X-Forward-For header. - Update to use SHA512 instead of MD5 for session identifier creation. - Fixes session creation for every view. - BREAKING: UTC used to set cookie duration. - BREAKING: Non-fresh logins now returns HTTP 401. - Support unicode user IDs in cookie. - Fixes user_logged_out signal invocation. - Support for per-Blueprint login views. - BREAKING: The `is_authenticated`, `is_active`, and `is_anonymous` members of the user class are now properties, not methods. Applications should update their user classes accordingly. - Various other improvements including documentation and code clean up. Version 0.2.11 -------------- Released on May 19th, 2014 - Fixes missing request loader invocation when authorization header exists. Version 0.2.10 -------------- Released on March 9th, 2014 - Generalized `request_loader` introduced; ability to log users in via customized callback over request. - Fixes request context dependency by explicitly checking `has_request_context`. - Fixes remember me issues since lazy user loading changes. Version 0.2.9 ------------- Released on December 28th, 2013 - Fixes anonymous user assignment. - Fixes localization in Python 3. Version 0.2.8 ------------- Released on December 21st 2013 - Support login via authorization header. This allows login via Basic Auth, for example. Useful in an API presentation context. - Ability to override user ID method name. This is useful if the ID getter is named differently than the default. - Session data is now only read when the user is requested. This can be beneficial for cookie and caching control when differenting between requests that use user information for rendering and ones where all users (including anonymous) get the same result (e.g. static pages) - BREAKING: User *must* always be accessed through the ``current_user`` local. This breaks any previous direct access to ``_request_ctx.top.user``. This is because user is not loaded until current_user is accessed. - Fixes unnecessary access to the session when the user is anonymous and session protection is active. see https://github.com/maxcountryman/flask-login/issues/120 - Fixes issue where order dependency of applying the login manager before dependent applications was required. see https://github.com/mattupstate/flask-principal/issues/22 - Fixes Python 3 ``UserMixin`` hashing. - Fixes incorrect documentation. Previous Versions ================= Prior to 0.2.8, no proper changelog was kept. flask-login-0.6.3/CONTRIBUTING.md000066400000000000000000000031341451774104400161670ustar00rootroot00000000000000# Contributor Guidelines Flask-Login is open source and will happily consider pull requests with bugfixes, documentation improvements, and ocassionally new features. Note that major changes will generally not be accepted. Before you submit an issue or pull request, please read the following guidlines. ## Submitting Issues Before you submit a new issue, **please review the [CHANGES](https://github.com/maxcountryman/flask-login/blob/master/CHANGES) document**. This is where you will find all major changes, including breaking changes, which may be causing your issue. Do not open a new issue before reading through CHANGES thoroughly and reviewing other open and closed issues. Duplicate issues will be closed and locked. Please do not open issues related to release deadlines: we will get to it when we can and in the meantime you are free to issue your own releases however you like. Issues should relate to specific bugs or feature requests. If this doesn't fit the profile, then please don't open an issue. ## Submitting a Pull Request If you'd like to submit PR, please make sure that all tests pass prior to submission. The README contains further instructions. ## Extended Documentation Sphinx-generated documentation can be found [here](https://flask-login.readthedocs.io/en/latest/). This page is updated automatically. Documentation for prior versions of the library may be found there as well. Always review this page when a problem is first encountered. ## Thanks Finally this project has seen contributions from many people and we owe them a debt of gratitude for taking time to improve the project. flask-login-0.6.3/LICENSE000066400000000000000000000020431451774104400147410ustar00rootroot00000000000000Copyright (c) 2011 Matthew Frazier 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-login-0.6.3/MANIFEST.in000066400000000000000000000002361451774104400154740ustar00rootroot00000000000000include CHANGES.md include LICENSE include README.md include tox.ini include requirements/*.txt graft docs prune docs/_build graft tests global-exclude *.pyc flask-login-0.6.3/README.md000066400000000000000000000110351451774104400152140ustar00rootroot00000000000000# Flask-Login ![Tests](https://github.com/maxcountryman/flask-login/workflows/Tests/badge.svg) [![coverage](https://coveralls.io/repos/maxcountryman/flask-login/badge.svg?branch=main&service=github)](https://coveralls.io/github/maxcountryman/flask-login?branch=main) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) Flask-Login provides user session management for Flask. It handles the common tasks of logging in, logging out, and remembering your users' sessions over extended periods of time. Flask-Login is not bound to any particular database system or permissions model. The only requirement is that your user objects implement a few methods, and that you provide a callback to the extension capable of loading users from their ID. ## Installation Install the extension with pip: ```sh $ pip install flask-login ``` ## Usage Once installed, the Flask-Login is easy to use. Let's walk through setting up a basic application. Also please note that this is a very basic guide: we will be taking shortcuts here that you should never take in a real application. To begin we'll set up a Flask app: ```python import flask app = flask.Flask(__name__) app.secret_key = 'super secret string' # Change this! ``` Flask-Login works via a login manager. To kick things off, we'll set up the login manager by instantiating it and telling it about our Flask app: ```python import flask_login login_manager = flask_login.LoginManager() login_manager.init_app(app) ``` To keep things simple we're going to use a dictionary to represent a database of users. In a real application, this would be an actual persistence layer. However it's important to point out this is a feature of Flask-Login: it doesn't care how your data is stored so long as you tell it how to retrieve it! ```python # Our mock database. users = {'foo@bar.tld': {'password': 'secret'}} ``` We also need to tell Flask-Login how to load a user from a Flask request and from its session. To do this we need to define our user object, a `user_loader` callback, and a `request_loader` callback. ```python class User(flask_login.UserMixin): pass @login_manager.user_loader def user_loader(email): if email not in users: return user = User() user.id = email return user @login_manager.request_loader def request_loader(request): email = request.form.get('email') if email not in users: return user = User() user.id = email return user ``` Now we're ready to define our views. We can start with a login view, which will populate the session with authentication bits. After that we can define a view that requires authentication. ```python @app.route('/login', methods=['GET', 'POST']) def login(): if flask.request.method == 'GET': return '''
''' email = flask.request.form['email'] if email in users and flask.request.form['password'] == users[email]['password']: user = User() user.id = email flask_login.login_user(user) return flask.redirect(flask.url_for('protected')) return 'Bad login' @app.route('/protected') @flask_login.login_required def protected(): return 'Logged in as: ' + flask_login.current_user.id ``` Finally we can define a view to clear the session and log users out: ```python @app.route('/logout') def logout(): flask_login.logout_user() return 'Logged out' ``` We now have a basic working application that makes use of session-based authentication. To round things off, we should provide a callback for login failures: ```python @login_manager.unauthorized_handler def unauthorized_handler(): return 'Unauthorized', 401 ``` Documentation for Flask-Login is available on [ReadTheDocs](https://flask-login.readthedocs.io/en/latest/). For complete understanding of available configuration, please refer to the [source code](https://github.com/maxcountryman/flask-login). ## Contributing We welcome contributions! If you would like to hack on Flask-Login, please follow these steps: 1. Fork this repository 2. Make your changes 3. Install the dev requirements with `pip install -r requirements/dev.txt` 4. Submit a pull request after running `tox` (ensure it does not error!) Please give us adequate time to review your submission. Thanks! flask-login-0.6.3/docs/000077500000000000000000000000001451774104400146655ustar00rootroot00000000000000flask-login-0.6.3/docs/Makefile000066400000000000000000000011761451774104400163320ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) flask-login-0.6.3/docs/_themes/000077500000000000000000000000001451774104400163115ustar00rootroot00000000000000flask-login-0.6.3/docs/_themes/LICENSE000066400000000000000000000033751451774104400173260ustar00rootroot00000000000000Copyright (c) 2010 by Armin Ronacher. Some rights reserved. Redistribution and use in source and binary forms of the theme, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. We kindly ask you to only use these themes in an unmodified manner just for Flask and Flask-related products, not for unrelated projects. If you like the visual style and want to use it for your own projects, please consider making some larger changes to the themes (such as changing font faces, sizes, colors or margins). THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. flask-login-0.6.3/docs/_themes/README000066400000000000000000000021051451774104400171670ustar00rootroot00000000000000Flask Sphinx Styles =================== This repository contains sphinx styles for Flask and Flask related projects. To use this style in your Sphinx documentation, follow this guide: 1. put this folder as _themes into your docs folder. Alternatively you can also use git submodules to check out the contents there. 2. add this to your conf.py: sys.path.append(os.path.abspath('_themes')) html_theme_path = ['_themes'] html_theme = 'flask' The following themes exist: - 'flask' - the standard flask documentation theme for large projects - 'flask_small' - small one-page theme. Intended to be used by very small addon libraries for flask. The following options exist for the flask_small theme: [options] index_logo = '' filename of a picture in _static to be used as replacement for the h1 in the index.rst file. index_logo_height = 120px height of the index logo github_fork = '' repository name on github for the "fork me" badge flask-login-0.6.3/docs/_themes/flask/000077500000000000000000000000001451774104400174115ustar00rootroot00000000000000flask-login-0.6.3/docs/_themes/flask/layout.html000066400000000000000000000011061451774104400216120ustar00rootroot00000000000000{%- extends "basic/layout.html" %} {%- block extrahead %} {{ super() }} {% if theme_touch_icon %} {% endif %} {% endblock %} {%- block relbar2 %}{% endblock %} {%- block footer %} {%- endblock %} flask-login-0.6.3/docs/_themes/flask/relations.html000066400000000000000000000011161451774104400222760ustar00rootroot00000000000000

Related Topics

flask-login-0.6.3/docs/_themes/flask/static/000077500000000000000000000000001451774104400207005ustar00rootroot00000000000000flask-login-0.6.3/docs/_themes/flask/static/flasky.css_t000066400000000000000000000141001451774104400232220ustar00rootroot00000000000000/* * flasky.css_t * ~~~~~~~~~~~~ * * :copyright: Copyright 2010 by Armin Ronacher. * :license: Flask Design License, see LICENSE for details. */ {% set page_width = '940px' %} {% set sidebar_width = '220px' %} @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'Georgia', serif; font-size: 17px; background-color: white; color: #000; margin: 0; padding: 0; } div.document { width: {{ page_width }}; margin: 30px auto 0 auto; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 {{ sidebar_width }}; } div.sphinxsidebar { width: {{ sidebar_width }}; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 0 30px; } img.floatingflask { padding: 0 0 10px 10px; float: right; } div.footer { width: {{ page_width }}; margin: 20px auto 30px auto; font-size: 14px; color: #888; text-align: right; } div.footer a { color: #888; } div.related { display: none; } div.sphinxsidebar a { color: #444; text-decoration: none; border-bottom: 1px dotted #999; } div.sphinxsidebar a:hover { border-bottom: 1px solid #999; } div.sphinxsidebar { font-size: 14px; line-height: 1.5; } div.sphinxsidebarwrapper { padding: 18px 10px; } div.sphinxsidebarwrapper p.logo { padding: 0 0 20px 0; margin: 0; text-align: center; } div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: 'Garamond', 'Georgia', serif; color: #444; font-size: 24px; font-weight: normal; margin: 0 0 5px 0; padding: 0; } div.sphinxsidebar h4 { font-size: 20px; } div.sphinxsidebar h3 a { color: #444; } div.sphinxsidebar p.logo a, div.sphinxsidebar h3 a, div.sphinxsidebar p.logo a:hover, div.sphinxsidebar h3 a:hover { border: none; } div.sphinxsidebar p { color: #555; margin: 10px 0; } div.sphinxsidebar ul { margin: 10px 0; padding: 0; color: #000; } div.sphinxsidebar input { border: 1px solid #ccc; font-family: 'Georgia', serif; font-size: 1em; } /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; } div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: #ddd; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: #444; background: #eaeaea; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { background: #fafafa; margin: 20px -30px; padding: 10px 30px; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } div.admonition tt.xref, div.admonition a tt { border-bottom: 1px solid #fafafa; } dd div.admonition { margin-left: -60px; padding-left: 60px; } div.admonition p.admonition-title { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } div.highlight { background-color: white; } dt:target, .highlight { background: #FAF3E8; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.9em; } img.screenshot { } tt.descname, tt.descclassname { font-size: 0.95em; } tt.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid #eee; background: #fdfdfd; font-size: 0.9em; } table.footnote + table.footnote { margin-top: -15px; border-top: none; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td.label { width: 0px; padding: 0.3em 0 0.3em 0.5em; } table.footnote td { padding: 0.3em 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } blockquote { margin: 0 0 0 30px; padding: 0; } ul, ol { margin: 10px 0 10px 30px; padding: 0; } pre { background: #eee; padding: 7px 30px; margin: 15px -30px; line-height: 1.3em; } dl pre, blockquote pre, li pre { margin-left: -60px; padding-left: 60px; } dl dl pre { margin-left: -90px; padding-left: 90px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, a tt { background-color: #FBFBFB; border-bottom: 1px solid white; } a.reference { text-decoration: none; border-bottom: 1px dotted #004B6B; } a.reference:hover { border-bottom: 1px solid #6D4100; } a.footnote-reference { text-decoration: none; font-size: 0.7em; vertical-align: top; border-bottom: 1px dotted #004B6B; } a.footnote-reference:hover { border-bottom: 1px solid #6D4100; } a:hover tt { background: #EEE; } flask-login-0.6.3/docs/_themes/flask/static/small_flask.css000066400000000000000000000017201451774104400237020ustar00rootroot00000000000000/* * small_flask.css_t * ~~~~~~~~~~~~~~~~~ * * :copyright: Copyright 2010 by Armin Ronacher. * :license: Flask Design License, see LICENSE for details. */ body { margin: 0; padding: 20px 30px; } div.documentwrapper { float: none; background: white; } div.sphinxsidebar { display: block; float: none; width: 102.5%; margin: 50px -30px -20px -30px; padding: 10px 20px; background: #333; color: white; } div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, div.sphinxsidebar h3 a { color: white; } div.sphinxsidebar a { color: #aaa; } div.sphinxsidebar p.logo { display: none; } div.document { width: 100%; margin: 0; } div.related { display: block; margin: 0; padding: 10px 0 20px 0; } div.related ul, div.related ul li { margin: 0; padding: 0; } div.footer { display: none; } div.bodywrapper { margin: 0; } div.body { min-height: 0; padding: 0; } flask-login-0.6.3/docs/_themes/flask/theme.conf000066400000000000000000000001711451774104400213610ustar00rootroot00000000000000[theme] inherit = basic stylesheet = flasky.css pygments_style = flask_theme_support.FlaskyStyle [options] touch_icon = flask-login-0.6.3/docs/_themes/flask_small/000077500000000000000000000000001451774104400206015ustar00rootroot00000000000000flask-login-0.6.3/docs/_themes/flask_small/layout.html000066400000000000000000000012551451774104400230070ustar00rootroot00000000000000{% extends "basic/layout.html" %} {% block header %} {{ super() }} {% if pagename == 'index' %}
{% endif %} {% endblock %} {% block footer %} {% if pagename == 'index' %}
{% endif %} {% endblock %} {# do not display relbars #} {% block relbar1 %}{% endblock %} {% block relbar2 %} {% if theme_github_fork %} Fork me on GitHub {% endif %} {% endblock %} {% block sidebar1 %}{% endblock %} {% block sidebar2 %}{% endblock %} flask-login-0.6.3/docs/_themes/flask_small/static/000077500000000000000000000000001451774104400220705ustar00rootroot00000000000000flask-login-0.6.3/docs/_themes/flask_small/static/flasky.css_t000066400000000000000000000107531451774104400244240ustar00rootroot00000000000000/* * flasky.css_t * ~~~~~~~~~~~~ * * Sphinx stylesheet -- flasky theme based on nature theme. * * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'Georgia', serif; font-size: 17px; color: #000; background: white; margin: 0; padding: 0; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 40px auto 0 auto; width: 700px; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; } img.floatingflask { padding: 0 0 10px 10px; float: right; } div.footer { text-align: right; color: #888; padding: 10px; font-size: 14px; width: 650px; margin: 0 auto 40px auto; } div.footer a { color: #888; text-decoration: underline; } div.related { line-height: 32px; color: #888; } div.related ul { padding: 0 0 0 10px; } div.related a { color: #444; } /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } div.body { padding-bottom: 40px; /* saved for footer */ } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; } {% if theme_index_logo %} div.indexwrapper h1 { text-indent: -999999px; background: url({{ theme_index_logo }}) no-repeat center center; height: {{ theme_index_logo_height }}; } {% endif %} div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: white; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: #444; background: #eaeaea; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { background: #fafafa; margin: 20px -30px; padding: 10px 30px; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } div.admonition p.admonition-title { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } div.highlight{ background-color: white; } dt:target, .highlight { background: #FAF3E8; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } div.warning { background-color: #ffe4e4; border: 1px solid #f66; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.85em; } img.screenshot { } tt.descname, tt.descclassname { font-size: 0.95em; } tt.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid #eee; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td { padding: 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } pre { padding: 0; margin: 15px -30px; padding: 8px; line-height: 1.3em; padding: 7px 30px; background: #eee; border-radius: 2px; -moz-border-radius: 2px; -webkit-border-radius: 2px; } dl pre { margin-left: -60px; padding-left: 60px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, a tt { background-color: #FBFBFB; } a:hover tt { background: #EEE; } flask-login-0.6.3/docs/_themes/flask_small/theme.conf000066400000000000000000000002701451774104400225510ustar00rootroot00000000000000[theme] inherit = basic stylesheet = flasky.css nosidebar = true pygments_style = flask_theme_support.FlaskyStyle [options] index_logo = '' index_logo_height = 120px github_fork = '' flask-login-0.6.3/docs/_themes/flask_theme_support.py000066400000000000000000000100521451774104400227370ustar00rootroot00000000000000# flasky extensions. flasky pygments style based on tango style from pygments.style import Style from pygments.token import Comment from pygments.token import Error from pygments.token import Generic from pygments.token import Keyword from pygments.token import Literal from pygments.token import Name from pygments.token import Number from pygments.token import Operator from pygments.token import Other from pygments.token import Punctuation from pygments.token import String from pygments.token import Whitespace class FlaskyStyle(Style): background_color = "#f8f8f8" default_style = "" styles = { # No corresponding class for the following: # Text: "", # class: '' Whitespace: "underline #f8f8f8", # class: 'w' Error: "#a40000 border:#ef2929", # class: 'err' Other: "#000000", # class 'x' Comment: "italic #8f5902", # class: 'c' Comment.Preproc: "noitalic", # class: 'cp' Keyword: "bold #004461", # class: 'k' Keyword.Constant: "bold #004461", # class: 'kc' Keyword.Declaration: "bold #004461", # class: 'kd' Keyword.Namespace: "bold #004461", # class: 'kn' Keyword.Pseudo: "bold #004461", # class: 'kp' Keyword.Reserved: "bold #004461", # class: 'kr' Keyword.Type: "bold #004461", # class: 'kt' Operator: "#582800", # class: 'o' Operator.Word: "bold #004461", # class: 'ow' - like keywords Punctuation: "bold #000000", # class: 'p' # because special names such as Name.Class, Name.Function, etc. # are not recognized as such later in the parsing, we choose them # to look the same as ordinary variables. Name: "#000000", # class: 'n' Name.Attribute: "#c4a000", # class: 'na' - to be revised Name.Builtin: "#004461", # class: 'nb' Name.Builtin.Pseudo: "#3465a4", # class: 'bp' Name.Class: "#000000", # class: 'nc' - to be revised Name.Constant: "#000000", # class: 'no' - to be revised Name.Decorator: "#888", # class: 'nd' - to be revised Name.Entity: "#ce5c00", # class: 'ni' Name.Exception: "bold #cc0000", # class: 'ne' Name.Function: "#000000", # class: 'nf' Name.Property: "#000000", # class: 'py' Name.Label: "#f57900", # class: 'nl' Name.Namespace: "#000000", # class: 'nn' - to be revised Name.Other: "#000000", # class: 'nx' Name.Tag: "bold #004461", # class: 'nt' - like a keyword Name.Variable: "#000000", # class: 'nv' - to be revised Name.Variable.Class: "#000000", # class: 'vc' - to be revised Name.Variable.Global: "#000000", # class: 'vg' - to be revised Name.Variable.Instance: "#000000", # class: 'vi' - to be revised Number: "#990000", # class: 'm' Literal: "#000000", # class: 'l' Literal.Date: "#000000", # class: 'ld' String: "#4e9a06", # class: 's' String.Backtick: "#4e9a06", # class: 'sb' String.Char: "#4e9a06", # class: 'sc' String.Doc: "italic #8f5902", # class: 'sd' - like a comment String.Double: "#4e9a06", # class: 's2' String.Escape: "#4e9a06", # class: 'se' String.Heredoc: "#4e9a06", # class: 'sh' String.Interpol: "#4e9a06", # class: 'si' String.Other: "#4e9a06", # class: 'sx' String.Regex: "#4e9a06", # class: 'sr' String.Single: "#4e9a06", # class: 's1' String.Symbol: "#4e9a06", # class: 'ss' Generic: "#000000", # class: 'g' Generic.Deleted: "#a40000", # class: 'gd' Generic.Emph: "italic #000000", # class: 'ge' Generic.Error: "#ef2929", # class: 'gr' Generic.Heading: "bold #000080", # class: 'gh' Generic.Inserted: "#00A000", # class: 'gi' Generic.Output: "#888", # class: 'go' Generic.Prompt: "#745334", # class: 'gp' Generic.Strong: "bold #000000", # class: 'gs' Generic.Subheading: "bold #800080", # class: 'gu' Generic.Traceback: "bold #a40000", # class: 'gt' } flask-login-0.6.3/docs/conf.py000066400000000000000000000170051451774104400161670ustar00rootroot00000000000000# # Flask-Login documentation build configuration file, created by # sphinx-quickstart on Tue Mar 15 18:40:10 2011. # # 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 # 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('.')) sys.path.append(os.path.join(os.path.dirname(__file__), "_themes")) # -- 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 = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode"] # 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-Login" copyright = "2011, Matthew Frazier" about = {} with open( os.path.join(os.path.dirname(__file__), "..", "src", "flask_login", "__about__.py") ) as f: exec(f.read(), about) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = about["__version__"] # The full version, including alpha/beta/rc tags. release = about["__version__"] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. default_role = "obj" # 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 = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "flask_small" # 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 = dict(github_fork="maxcountryman/flask-login", index_logo=False) # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ["_themes"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # 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'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "Flask-Logindoc" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). # latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). # latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( "index", "Flask-Login.tex", "Flask-Login Documentation", "Matthew Frazier", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Additional stuff for the LaTeX preamble. # latex_preamble = '' # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ("index", "flask-login", "Flask-Login Documentation", ["Matthew Frazier"], 1) ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "flask": ("http://flask.pocoo.org/docs/", None), } auto_content = "both" flask-login-0.6.3/docs/index.rst000066400000000000000000000606701451774104400165370ustar00rootroot00000000000000=========== Flask-Login =========== .. currentmodule:: flask_login Flask-Login provides user session management for Flask. It handles the common tasks of logging in, logging out, and remembering your users' sessions over extended periods of time. It will: - Store the active user's ID in the session, and let you log them in and out easily. - Let you restrict views to logged-in (or logged-out) users. - Handle the normally-tricky "remember me" functionality. - Help protect your users' sessions from being stolen by cookie thieves. - Possibly integrate with Flask-Principal or other authorization extensions later on. However, it does not: - Impose a particular database or other storage method on you. You are entirely in charge of how the user is loaded. - Restrict you to using usernames and passwords, OpenIDs, or any other method of authenticating. - Handle permissions beyond "logged in or not." - Handle user registration or account recovery. .. contents:: :local: :backlinks: none Installation ============ Install the extension with pip:: $ pip install flask-login Configuring your Application ============================ The most important part of an application that uses Flask-Login is the `LoginManager` class. You should create one for your application somewhere in your code, like this:: from flask_login import LoginManager login_manager = LoginManager() The login manager contains the code that lets your application and Flask-Login work together, such as how to load a user from an ID, where to send users when they need to log in, and the like. Once the actual application object has been created, you can configure it for login with:: login_manager.init_app(app) By default, Flask-Login uses sessions for authentication. This means you must set the secret key on your application, otherwise Flask will give you an error message telling you to do so. See the `Flask documentation on sessions`_ to see how to set a secret key. *Warning:* Make SURE to use the given command in the "How to generate good secret keys" section to generate your own secret key. DO NOT use the example one. For a complete understanding of available configuration keys, please refer to the `source code`_. How it Works ============ You will need to provide a `~LoginManager.user_loader` callback. This callback is used to reload the user object from the user ID stored in the session. It should take the `str` ID of a user, and return the corresponding user object. For example:: @login_manager.user_loader def load_user(user_id): return User.get(user_id) It should return `None` (**not raise an exception**) if the ID is not valid. (In that case, the ID will manually be removed from the session and processing will continue.) Your User Class =============== The class that you use to represent users needs to implement these properties and methods: `is_authenticated` This property should return `True` if the user is authenticated, i.e. they have provided valid credentials. (Only authenticated users will fulfill the criteria of `login_required`.) `is_active` This property should return `True` if this is an active user - in addition to being authenticated, they also have activated their account, not been suspended, or any condition your application has for rejecting an account. Inactive accounts may not log in (without being forced of course). `is_anonymous` This property should return `True` if this is an anonymous user. (Actual users should return `False` instead.) `get_id()` This method must return a `str` that uniquely identifies this user, and can be used to load the user from the `~LoginManager.user_loader` callback. Note that this **must** be a `str` - if the ID is natively an `int` or some other type, you will need to convert it to `str`. To make implementing a user class easier, you can inherit from `UserMixin`, which provides default implementations for all of these properties and methods. (It's not required, though.) Login Example ============= Once a user has authenticated, you log them in with the `login_user` function. For example: .. code-block:: python @app.route('/login', methods=['GET', 'POST']) def login(): # Here we use a class of some kind to represent and validate our # client-side form data. For example, WTForms is a library that will # handle this for us, and we use a custom LoginForm to validate. form = LoginForm() if form.validate_on_submit(): # Login and validate the user. # user should be an instance of your `User` class login_user(user) flask.flash('Logged in successfully.') next = flask.request.args.get('next') # is_safe_url should check if the url is safe for redirects. # See http://flask.pocoo.org/snippets/62/ for an example. if not is_safe_url(next): return flask.abort(400) return flask.redirect(next or flask.url_for('index')) return flask.render_template('login.html', form=form) *Warning:* You MUST validate the value of the `next` parameter. If you do not, your application will be vulnerable to open redirects. For an example implementation of `is_safe_url` see `this Flask Snippet`_. It's that simple. You can then access the logged-in user with the `current_user` proxy, which is available in every template:: {% if current_user.is_authenticated %} Hi {{ current_user.name }}! {% endif %} Views that require your users to be logged in can be decorated with the `login_required` decorator:: @app.route("/settings") @login_required def settings(): pass When the user is ready to log out:: @app.route("/logout") @login_required def logout(): logout_user() return redirect(somewhere) They will be logged out, and any cookies for their session will be cleaned up. Customizing the Login Process ============================= By default, when a user attempts to access a `login_required` view without being logged in, Flask-Login will flash a message and redirect them to the log in view. (If the login view is not set, it will abort with a 401 error.) The name of the log in view can be set as `LoginManager.login_view`. For example:: login_manager.login_view = "users.login" The default message flashed is ``Please log in to access this page.`` To customize the message, set `LoginManager.login_message`:: login_manager.login_message = u"Bonvolu ensaluti por uzi tiun paĝon." To customize the message category, set `LoginManager.login_message_category`:: login_manager.login_message_category = "info" When the log in view is redirected to, it will have a ``next`` variable in the query string, which is the page that the user was trying to access. Alternatively, if `USE_SESSION_FOR_NEXT` is `True`, the page is stored in the session under the key ``next``. If you would like to customize the process further, decorate a function with `LoginManager.unauthorized_handler`:: @login_manager.unauthorized_handler def unauthorized(): # do stuff return a_response For example: You are using Flask Login with Flask Restful. In your API (blueprint named as api) you don't wanna redirect to login page but return Unauthorized status code .:: from flask import redirect, url_for, request from http import HTTPStatus @login_manager.unauthorized_handler def unauthorized(): if request.blueprint == 'api': abort(HTTPStatus.UNAUTHORIZED) return redirect(url_for('site.login')) Login using Authorization header ================================ .. Caution:: This method will be deprecated; use the `~LoginManager.request_loader` below instead. Sometimes you want to support Basic Auth login using the `Authorization` header, such as for api requests. To support login via header you will need to provide a `~LoginManager.header_loader` callback. This callback should behave the same as your `~LoginManager.user_loader` callback, except that it accepts a header value instead of a user id. For example:: @login_manager.header_loader def load_user_from_header(header_val): header_val = header_val.replace('Basic ', '', 1) try: header_val = base64.b64decode(header_val) except TypeError: pass return User.query.filter_by(api_key=header_val).first() By default the `Authorization` header's value is passed to your `~LoginManager.header_loader` callback. You can change the header used with the `AUTH_HEADER_NAME` configuration. Custom Login using Request Loader ================================= Sometimes you want to login users without using cookies, such as using header values or an api key passed as a query argument. In these cases, you should use the `~LoginManager.request_loader` callback. This callback should behave the same as your `~LoginManager.user_loader` callback, except that it accepts the Flask request instead of a user_id. For example, to support login from both a url argument and from Basic Auth using the `Authorization` header:: @login_manager.request_loader def load_user_from_request(request): # first, try to login using the api_key url arg api_key = request.args.get('api_key') if api_key: user = User.query.filter_by(api_key=api_key).first() if user: return user # next, try to login using Basic Auth api_key = request.headers.get('Authorization') if api_key: api_key = api_key.replace('Basic ', '', 1) try: api_key = base64.b64decode(api_key) except TypeError: pass user = User.query.filter_by(api_key=api_key).first() if user: return user # finally, return None if both methods did not login the user return None Anonymous Users =============== By default, when a user is not actually logged in, `current_user` is set to an `AnonymousUserMixin` object. It has the following properties and methods: - `is_active` is `False` - `is_authenticated` is `False` - `is_anonymous` is `True` - `get_id()` returns `None` If you have custom requirements for anonymous users (for example, they need to have a permissions field), you can provide a callable (either a class or factory function) that creates anonymous users to the `LoginManager` with:: login_manager.anonymous_user = MyAnonymousUser Remember Me =========== By default, when the user closes their browser the Flask Session is deleted and the user is logged out. "Remember Me" prevents the user from accidentally being logged out when they close their browser. This does **NOT** mean remembering or pre-filling the user's username or password in a login form after the user has logged out. "Remember Me" functionality can be tricky to implement. However, Flask-Login makes it nearly transparent - just pass ``remember=True`` to the `login_user` call. A cookie will be saved on the user's computer, and then Flask-Login will automatically restore the user ID from that cookie if it is not in the session. The amount of time before the cookie expires can be set with the `REMEMBER_COOKIE_DURATION` configuration or it can be passed to `login_user`. The cookie is tamper-proof, so if the user tampers with it (i.e. inserts someone else's user ID in place of their own), the cookie will merely be rejected, as if it was not there. That level of functionality is handled automatically. However, you can (and should, if your application handles any kind of sensitive data) provide additional infrastructure to increase the security of your remember cookies. Alternative Tokens ================== Using the user ID as the value of the remember token means you must change the user's ID to invalidate their login sessions. One way to improve this is to use an alternative user id instead of the user's ID. For example:: @login_manager.user_loader def load_user(user_id): return User.query.filter_by(alternative_id=user_id).first() Then the `~UserMixin.get_id` method of your User class would return the alternative id instead of the user's primary ID:: def get_id(self): return str(self.alternative_id) This way you are free to change the user's alternative id to a new randomly generated value when the user changes their password, which would ensure their old authentication sessions will cease to be valid. Note that the alternative id must still uniquely identify the user... think of it as a second user ID. Fresh Logins ============ When a user logs in, their session is marked as "fresh," which indicates that they actually authenticated on that session. When their session is destroyed and they are logged back in with a "remember me" cookie, it is marked as "non-fresh." `login_required` does not differentiate between freshness, which is fine for most pages. However, sensitive actions like changing one's personal information should require a fresh login. (Actions like changing one's password should always require a password re-entry regardless.) `fresh_login_required`, in addition to verifying that the user is logged in, will also ensure that their login is fresh. If not, it will send them to a page where they can re-enter their credentials. You can customize its behavior in the same ways as you can customize `login_required`, by setting `LoginManager.refresh_view`, `~LoginManager.needs_refresh_message`, and `~LoginManager.needs_refresh_message_category`:: login_manager.refresh_view = "accounts.reauthenticate" login_manager.needs_refresh_message = ( u"To protect your account, please reauthenticate to access this page." ) login_manager.needs_refresh_message_category = "info" Or by providing your own callback to handle refreshing:: @login_manager.needs_refresh_handler def refresh(): # do stuff return a_response To mark a session as fresh again, call the `confirm_login` function. Cookie Settings =============== The details of the cookie can be customized in the application settings. ====================================== ================================================= `REMEMBER_COOKIE_NAME` The name of the cookie to store the "remember me" information in. **Default:** ``remember_token`` `REMEMBER_COOKIE_DURATION` The amount of time before the cookie expires, as a `datetime.timedelta` object or integer seconds. **Default:** 365 days (1 non-leap Gregorian year) `REMEMBER_COOKIE_DOMAIN` If the "Remember Me" cookie should cross domains, set the domain value here (i.e. ``.example.com`` would allow the cookie to be used on all subdomains of ``example.com``). **Default:** `None` `REMEMBER_COOKIE_PATH` Limits the "Remember Me" cookie to a certain path. **Default:** ``/`` `REMEMBER_COOKIE_SECURE` Restricts the "Remember Me" cookie's scope to secure channels (typically HTTPS). **Default:** `False` `REMEMBER_COOKIE_HTTPONLY` Prevents the "Remember Me" cookie from being accessed by client-side scripts. **Default:** `True` `REMEMBER_COOKIE_REFRESH_EACH_REQUEST` If set to `True` the cookie is refreshed on every request, which bumps the lifetime. Works like Flask's `SESSION_REFRESH_EACH_REQUEST`. **Default:** `False` `REMEMBER_COOKIE_SAMESITE` Restricts the "Remember Me" cookie to first-party or same-site context. **Default:** `None` ====================================== ================================================= Session Protection ================== While the features above help secure your "Remember Me" token from cookie thieves, the session cookie is still vulnerable. Flask-Login includes session protection to help prevent your users' sessions from being stolen. You can configure session protection on the `LoginManager`, and in the app's configuration. If it is enabled, it can operate in either `basic` or `strong` mode. To set it on the `LoginManager`, set the `~LoginManager.session_protection` attribute to ``"basic"`` or ``"strong"``:: login_manager.session_protection = "strong" Or, to disable it:: login_manager.session_protection = None By default, it is activated in ``"basic"`` mode. It can be disabled in the app's configuration by setting the `SESSION_PROTECTION` setting to `None`, ``"basic"``, or ``"strong"``. When session protection is active, each request, it generates an identifier for the user's computer (basically, a secure hash of the IP address and user agent). If the session does not have an associated identifier, the one generated will be stored. If it has an identifier, and it matches the one generated, then the request is OK. If the identifiers do not match in `basic` mode, or when the session is permanent, then the session will simply be marked as non-fresh, and anything requiring a fresh login will force the user to re-authenticate. (Of course, you must be already using fresh logins where appropriate for this to have an effect.) If the identifiers do not match in `strong` mode for a non-permanent session, then the entire session (as well as the remember token if it exists) is deleted. Disabling Session Cookie for APIs ================================= When authenticating to APIs, you might want to disable setting the Flask Session cookie. To do this, use a custom session interface that skips saving the session depending on a flag you set on the request. For example:: from flask import g from flask.sessions import SecureCookieSessionInterface from flask_login import user_loaded_from_request @user_loaded_from_request.connect def user_loaded_from_request(app, user=None): g.login_via_request = True class CustomSessionInterface(SecureCookieSessionInterface): """Prevent creating session from API requests.""" def save_session(self, *args, **kwargs): if g.get('login_via_request'): return return super(CustomSessionInterface, self).save_session(*args, **kwargs) app.session_interface = CustomSessionInterface() @user_loaded_from_request.connect def user_loaded_from_request(self, user=None): g.login_via_request = True This prevents setting the Flask Session cookie whenever the user authenticated using your `~LoginManager.request_loader`. Automated Testing ================= To make it easier for you to write automated tests, Flask-Login provides a simple, custom test client class that will set the user's login cookie for you: `~FlaskLoginClient`. To use this custom test client class, assign it to the :attr:`test_client_class ` attribute on your application object, like this:: from flask_login import FlaskLoginClient app.test_client_class = FlaskLoginClient Next, use the :meth:`app.test_client() ` method to make a test client, as you normally do. However, now you can pass a user object to this method, and your client will be automatically logged in with this user! .. code-block:: python def test_request_with_logged_in_user(): user = User.query.get(1) with app.test_client(user=user) as client: # This request has user 1 already logged in! client.get("/") You may also pass ``fresh_login`` (``bool``, defaults to ``True``) to mark the current login as fresh or non-fresh. Note that you must use keyword arguments, not positional arguments. E.g. ``test_client(user=user)`` will work, but ``test_client(user)`` will not. Due to the way this custom test client class is implemented, you may have to disable **session protection** to have your tests work properly. If session protection is enabled, login sessions will be marked non-fresh in `basic` mode or outright rejected in `strong` mode when performing requests with the test client. Localization ============ By default, the `LoginManager` uses ``flash`` to display messages when a user is required to log in. These messages are in English. If you require localization, set the `localize_callback` attribute of `LoginManager` to a function to be called with these messages before they're sent to ``flash``, e.g. ``gettext``. This function will be called with the message and its return value will be sent to ``flash`` instead. API Documentation ================= This documentation is automatically generated from Flask-Login's source code. Configuring Login ----------------- .. module:: flask_login .. autoclass:: LoginManager .. automethod:: init_app .. automethod:: unauthorized .. automethod:: needs_refresh .. rubric:: General Configuration .. automethod:: user_loader .. automethod:: request_loader .. attribute:: anonymous_user A class or factory function that produces an anonymous user, which is used when no one is logged in. .. rubric:: `unauthorized` Configuration .. attribute:: login_view The name of the view to redirect to when the user needs to log in. (This can be an absolute URL as well, if your authentication machinery is external to your application.) .. attribute:: blueprint_login_views This is similar to login_view, except it is used when working with blueprints. It is a dictionary that can store multiple views to redirect to for different blueprints. The redirects are listed in the form of key as the blueprint's name and value as the redirect to route. .. attribute:: login_message The message to flash when a user is redirected to the login page. .. automethod:: unauthorized_handler .. rubric:: `needs_refresh` Configuration .. attribute:: refresh_view The name of the view to redirect to when the user needs to reauthenticate. .. attribute:: needs_refresh_message The message to flash when a user is redirected to the reauthentication page. .. automethod:: needs_refresh_handler Login Mechanisms ---------------- .. data:: current_user A proxy for the current user. .. autofunction:: login_fresh .. autofunction:: login_remembered .. autofunction:: login_user .. autofunction:: logout_user .. autofunction:: confirm_login Protecting Views ---------------- .. autofunction:: login_required .. autofunction:: fresh_login_required User Object Helpers ------------------- .. autoclass:: UserMixin :members: .. autoclass:: AnonymousUserMixin :members: Utilities --------- .. autofunction:: login_url .. autoclass:: FlaskLoginClient Signals ------- See the `Flask documentation on signals`_ for information on how to use these signals in your code. .. data:: user_logged_in Sent when a user is logged in. In addition to the app (which is the sender), it is passed `user`, which is the user being logged in. .. data:: user_logged_out Sent when a user is logged out. In addition to the app (which is the sender), it is passed `user`, which is the user being logged out. .. data:: user_login_confirmed Sent when a user's login is confirmed, marking it as fresh. (It is not called for a normal login.) It receives no additional arguments besides the app. .. data:: user_unauthorized Sent when the `unauthorized` method is called on a `LoginManager`. It receives no additional arguments besides the app. .. data:: user_needs_refresh Sent when the `needs_refresh` method is called on a `LoginManager`. It receives no additional arguments besides the app. .. data:: session_protected Sent whenever session protection takes effect, and a session is either marked non-fresh or deleted. It receives no additional arguments besides the app. .. _source code: https://github.com/maxcountryman/flask-login/tree/main/src/flask_login .. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/ .. _this Flask Snippet: https://web.archive.org/web/20120517003641/http://flask.pocoo.org/snippets/62/ .. _Flask documentation on sessions: http://flask.pocoo.org/docs/quickstart/#sessions flask-login-0.6.3/docs/make.bat000066400000000000000000000014011451774104400162660ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd flask-login-0.6.3/requirements/000077500000000000000000000000001451774104400164605ustar00rootroot00000000000000flask-login-0.6.3/requirements/ci-release.in000066400000000000000000000000341451774104400210160ustar00rootroot00000000000000build twine readme-renderer flask-login-0.6.3/requirements/ci-release.txt000066400000000000000000000023051451774104400212320ustar00rootroot00000000000000# SHA1:f12dcf05047085cd0e6558c5d19d75fe89264911 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # build==1.0.3 # via -r requirements/ci-release.in certifi==2023.7.22 # via requests charset-normalizer==3.3.1 # via requests docutils==0.20.1 # via readme-renderer idna==3.4 # via requests importlib-metadata==6.8.0 # via # keyring # twine jaraco-classes==3.3.0 # via keyring keyring==24.2.0 # via twine markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py more-itertools==10.1.0 # via jaraco-classes nh3==0.2.14 # via readme-renderer packaging==23.2 # via build pkginfo==1.9.6 # via twine pygments==2.16.1 # via # readme-renderer # rich pyproject-hooks==1.0.0 # via build readme-renderer==42.0 # via # -r requirements/ci-release.in # twine requests==2.31.0 # via # requests-toolbelt # twine requests-toolbelt==1.0.0 # via twine rfc3986==2.0.0 # via twine rich==13.6.0 # via twine twine==4.0.2 # via -r requirements/ci-release.in urllib3==2.0.7 # via # requests # twine zipp==3.17.0 # via importlib-metadata flask-login-0.6.3/requirements/ci-tests.in000066400000000000000000000000161451774104400205400ustar00rootroot00000000000000tox coveralls flask-login-0.6.3/requirements/ci-tests.txt000066400000000000000000000016131451774104400207550ustar00rootroot00000000000000# SHA1:da99472fcc215ae404f9516210d1a699f8ff759c # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # cachetools==5.3.2 # via tox certifi==2023.7.22 # via requests chardet==5.2.0 # via tox charset-normalizer==3.3.1 # via requests colorama==0.4.6 # via tox coverage==6.5.0 # via coveralls coveralls==3.3.1 # via -r requirements/ci-tests.in distlib==0.3.7 # via virtualenv docopt==0.6.2 # via coveralls filelock==3.13.0 # via # tox # virtualenv idna==3.4 # via requests packaging==23.2 # via # pyproject-api # tox platformdirs==3.11.0 # via # tox # virtualenv pluggy==1.3.0 # via tox pyproject-api==1.6.1 # via tox requests==2.31.0 # via coveralls tox==4.11.3 # via -r requirements/ci-tests.in urllib3==2.0.7 # via requests virtualenv==20.24.6 # via tox flask-login-0.6.3/requirements/dev.in000066400000000000000000000000711451774104400175640ustar00rootroot00000000000000-r docs.in -r style.in -r tests.in pip-compile-multi tox flask-login-0.6.3/requirements/dev.txt000066400000000000000000000014021451774104400177740ustar00rootroot00000000000000# SHA1:862a5e687ccba7956bb09d428fcd447a7497bac8 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # -r docs.txt -r style.txt -r tests.txt build==1.0.3 # via pip-tools cachetools==5.3.2 # via tox chardet==5.2.0 # via tox click==8.1.7 # via # pip-compile-multi # pip-tools colorama==0.4.6 # via tox pip-compile-multi==2.6.3 # via -r requirements/dev.in pip-tools==7.3.0 # via pip-compile-multi pyproject-api==1.6.1 # via tox pyproject-hooks==1.0.0 # via build toposort==1.10 # via pip-compile-multi tox==4.11.3 # via -r requirements/dev.in wheel==0.41.2 # via pip-tools # The following packages are considered to be unsafe in a requirements file: # pip # setuptools flask-login-0.6.3/requirements/docs.in000066400000000000000000000000071451774104400177350ustar00rootroot00000000000000Sphinx flask-login-0.6.3/requirements/docs.txt000066400000000000000000000021551451774104400201540ustar00rootroot00000000000000# SHA1:a8bbd38a4fa95159d3262290300fa4db02a5b792 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # alabaster==0.7.13 # via sphinx babel==2.13.1 # via sphinx certifi==2023.7.22 # via requests charset-normalizer==3.3.1 # via requests docutils==0.20.1 # via sphinx idna==3.4 # via requests imagesize==1.4.1 # via sphinx jinja2==3.1.2 # via sphinx markupsafe==2.1.3 # via jinja2 packaging==23.2 # via sphinx pygments==2.16.1 # via sphinx requests==2.31.0 # via sphinx snowballstemmer==2.2.0 # via sphinx sphinx==7.2.6 # via # -r requirements/docs.in # sphinxcontrib-applehelp # sphinxcontrib-devhelp # sphinxcontrib-htmlhelp # sphinxcontrib-qthelp # sphinxcontrib-serializinghtml sphinxcontrib-applehelp==1.0.7 # via sphinx sphinxcontrib-devhelp==1.0.5 # via sphinx sphinxcontrib-htmlhelp==2.0.4 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==1.0.6 # via sphinx sphinxcontrib-serializinghtml==1.1.9 # via sphinx urllib3==2.0.7 # via requests flask-login-0.6.3/requirements/style.in000066400000000000000000000000131451774104400201420ustar00rootroot00000000000000pre-commit flask-login-0.6.3/requirements/style.txt000066400000000000000000000011141451774104400203560ustar00rootroot00000000000000# SHA1:5a0b1bb22ae805d8aebba0f3bf05ab91aceae0d8 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # cfgv==3.4.0 # via pre-commit distlib==0.3.7 # via virtualenv filelock==3.13.0 # via virtualenv identify==2.5.31 # via pre-commit nodeenv==1.8.0 # via pre-commit platformdirs==3.11.0 # via virtualenv pre-commit==3.5.0 # via -r requirements/style.in pyyaml==6.0.1 # via pre-commit virtualenv==20.24.6 # via pre-commit # The following packages are considered to be unsafe in a requirements file: # setuptools flask-login-0.6.3/requirements/tests-min.in000066400000000000000000000001371451774104400207340ustar00rootroot00000000000000flask==1.0.4 werkzeug==1.0.1 itsdangerous==1.1.0 jinja2==2.10.3 markupsafe==1.1.1 click==7.1.2 flask-login-0.6.3/requirements/tests-min.txt000066400000000000000000000011111451774104400211360ustar00rootroot00000000000000# SHA1:4a5a27b5bd8d619c962f094f643f3db17ae428aa # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # click==7.1.2 # via # -r requirements/tests-min.in # flask flask==1.0.4 # via -r requirements/tests-min.in itsdangerous==1.1.0 # via # -r requirements/tests-min.in # flask jinja2==2.10.3 # via # -r requirements/tests-min.in # flask markupsafe==1.1.1 # via # -r requirements/tests-min.in # jinja2 werkzeug==1.0.1 # via # -r requirements/tests-min.in # flask flask-login-0.6.3/requirements/tests.in000066400000000000000000000000611451774104400201470ustar00rootroot00000000000000blinker coverage asgiref pytest semantic_version flask-login-0.6.3/requirements/tests.txt000066400000000000000000000007671451774104400203750ustar00rootroot00000000000000# SHA1:36a6023d0127e468d03ef122f5ca862c556f8090 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # asgiref==3.7.2 # via -r requirements/tests.in blinker==1.6.3 # via -r requirements/tests.in coverage==7.3.2 # via -r requirements/tests.in iniconfig==2.0.0 # via pytest packaging==23.2 # via pytest pluggy==1.3.0 # via pytest pytest==7.4.3 # via -r requirements/tests.in semantic-version==2.10.0 # via -r requirements/tests.in flask-login-0.6.3/setup.cfg000066400000000000000000000036141451774104400155620ustar00rootroot00000000000000[metadata] name = Flask-Login version = attr: flask_login.__about__.__version__ url = https://github.com/maxcountryman/flask-login project_urls = Documentation = https://flask-login.readthedocs.io/ Changes = https://github.com/maxcountryman/flask-login/blob/main/CHANGES.md Source Code = https://github.com/maxcountryman/flask-login Issue Tracker = https://github.com/maxcountryman/flask-login/issues license = MIT author = Matthew Frazier author_email = leafstormrush@gmail.com maintainer = Max Countryman description = User authentication and session management for Flask. long_description = file: README.md long_description_content_type = text/markdown classifiers = Development Status :: 4 - Beta 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 [options] packages = find: package_dir = = src include_package_data = True python_requires = >= 3.7 # Dependencies are in setup.py for GitHub's dependency graph. [options.packages.find] where = src [tool:pytest] testpaths = tests filterwarnings = error [coverage:run] source = flask_login [coverage:paths] source = src */site-packages [flake8] # B = bugbear # E = pycodestyle errors # F = flake8 pyflakes # W = pycodestyle warnings # B9 = bugbear opinions # ISC = implicit str concat select = B, E, F, W, B9, ISC ignore = # slice notation whitespace, invalid E203 # line length, handled by bugbear B950 E501 # bare except, handled by bugbear B001 E722 # bin op line break, invalid W503 # up to 88 allowed by bugbear B950 max-line-length = 80 per-file-ignores = # __init__ exports names src/flask_login/__init__.py: F401 flask-login-0.6.3/setup.py000066400000000000000000000003341451774104400154470ustar00rootroot00000000000000from setuptools import setup # Metadata goes in setup.cfg. These are here for GitHub's dependency graph. setup( name="Flask-Login", install_requires=[ "Flask>=1.0.4", "Werkzeug>=1.0.1", ], ) flask-login-0.6.3/src/000077500000000000000000000000001451774104400145245ustar00rootroot00000000000000flask-login-0.6.3/src/flask_login/000077500000000000000000000000001451774104400170145ustar00rootroot00000000000000flask-login-0.6.3/src/flask_login/__about__.py000066400000000000000000000006051451774104400212750ustar00rootroot00000000000000__title__ = "Flask-Login" __description__ = "User session management for Flask" __url__ = "https://github.com/maxcountryman/flask-login" __version_info__ = ("0", "6", "3") __version__ = ".".join(__version_info__) __author__ = "Matthew Frazier" __author_email__ = "leafstormrush@gmail.com" __maintainer__ = "Max Countryman" __license__ = "MIT" __copyright__ = "(c) 2011 by Matthew Frazier" flask-login-0.6.3/src/flask_login/__init__.py000066400000000000000000000051711451774104400211310ustar00rootroot00000000000000from .__about__ import __version__ from .config import AUTH_HEADER_NAME from .config import COOKIE_DURATION from .config import COOKIE_HTTPONLY from .config import COOKIE_NAME from .config import COOKIE_SECURE from .config import ID_ATTRIBUTE from .config import LOGIN_MESSAGE from .config import LOGIN_MESSAGE_CATEGORY from .config import REFRESH_MESSAGE from .config import REFRESH_MESSAGE_CATEGORY from .login_manager import LoginManager from .mixins import AnonymousUserMixin from .mixins import UserMixin from .signals import session_protected from .signals import user_accessed from .signals import user_loaded_from_cookie from .signals import user_loaded_from_request from .signals import user_logged_in from .signals import user_logged_out from .signals import user_login_confirmed from .signals import user_needs_refresh from .signals import user_unauthorized from .test_client import FlaskLoginClient from .utils import confirm_login from .utils import current_user from .utils import decode_cookie from .utils import encode_cookie from .utils import fresh_login_required from .utils import login_fresh from .utils import login_remembered from .utils import login_required from .utils import login_url from .utils import login_user from .utils import logout_user from .utils import make_next_param from .utils import set_login_view __all__ = [ "__version__", "AUTH_HEADER_NAME", "COOKIE_DURATION", "COOKIE_HTTPONLY", "COOKIE_NAME", "COOKIE_SECURE", "ID_ATTRIBUTE", "LOGIN_MESSAGE", "LOGIN_MESSAGE_CATEGORY", "REFRESH_MESSAGE", "REFRESH_MESSAGE_CATEGORY", "LoginManager", "AnonymousUserMixin", "UserMixin", "session_protected", "user_accessed", "user_loaded_from_cookie", "user_loaded_from_request", "user_logged_in", "user_logged_out", "user_login_confirmed", "user_needs_refresh", "user_unauthorized", "FlaskLoginClient", "confirm_login", "current_user", "decode_cookie", "encode_cookie", "fresh_login_required", "login_fresh", "login_remembered", "login_required", "login_url", "login_user", "logout_user", "make_next_param", "set_login_view", ] def __getattr__(name): if name == "user_loaded_from_header": import warnings from .signals import _user_loaded_from_header warnings.warn( "'user_loaded_from_header' is deprecated and will be" " removed in Flask-Login 0.7. Use" " 'user_loaded_from_request' instead.", DeprecationWarning, stacklevel=2, ) return _user_loaded_from_header raise AttributeError(name) flask-login-0.6.3/src/flask_login/config.py000066400000000000000000000034251451774104400206370ustar00rootroot00000000000000from datetime import timedelta #: The default name of the "remember me" cookie (``remember_token``) COOKIE_NAME = "remember_token" #: The default time before the "remember me" cookie expires (365 days). COOKIE_DURATION = timedelta(days=365) #: Whether the "remember me" cookie requires Secure; defaults to ``False`` COOKIE_SECURE = False #: Whether the "remember me" cookie uses HttpOnly or not; defaults to ``True`` COOKIE_HTTPONLY = True #: Whether the "remember me" cookie requires same origin; defaults to ``None`` COOKIE_SAMESITE = None #: The default flash message to display when users need to log in. LOGIN_MESSAGE = "Please log in to access this page." #: The default flash message category to display when users need to log in. LOGIN_MESSAGE_CATEGORY = "message" #: The default flash message to display when users need to reauthenticate. REFRESH_MESSAGE = "Please reauthenticate to access this page." #: The default flash message category to display when users need to #: reauthenticate. REFRESH_MESSAGE_CATEGORY = "message" #: The default attribute to retreive the str id of the user ID_ATTRIBUTE = "get_id" #: Default name of the auth header (``Authorization``) AUTH_HEADER_NAME = "Authorization" #: A set of session keys that are populated by Flask-Login. Use this set to #: purge keys safely and accurately. SESSION_KEYS = { "_user_id", "_remember", "_remember_seconds", "_id", "_fresh", "next", } #: A set of HTTP methods which are exempt from `login_required` and #: `fresh_login_required`. By default, this is just ``OPTIONS``. EXEMPT_METHODS = {"OPTIONS"} #: If true, the page the user is attempting to access is stored in the session #: rather than a url parameter when redirecting to the login view; defaults to #: ``False``. USE_SESSION_FOR_NEXT = False flask-login-0.6.3/src/flask_login/login_manager.py000066400000000000000000000471511451774104400222000ustar00rootroot00000000000000from datetime import datetime from datetime import timedelta from flask import abort from flask import current_app from flask import flash from flask import g from flask import has_app_context from flask import redirect from flask import request from flask import session from .config import AUTH_HEADER_NAME from .config import COOKIE_DURATION from .config import COOKIE_HTTPONLY from .config import COOKIE_NAME from .config import COOKIE_SAMESITE from .config import COOKIE_SECURE from .config import ID_ATTRIBUTE from .config import LOGIN_MESSAGE from .config import LOGIN_MESSAGE_CATEGORY from .config import REFRESH_MESSAGE from .config import REFRESH_MESSAGE_CATEGORY from .config import SESSION_KEYS from .config import USE_SESSION_FOR_NEXT from .mixins import AnonymousUserMixin from .signals import session_protected from .signals import user_accessed from .signals import user_loaded_from_cookie from .signals import user_loaded_from_request from .signals import user_needs_refresh from .signals import user_unauthorized from .utils import _create_identifier from .utils import _user_context_processor from .utils import decode_cookie from .utils import encode_cookie from .utils import expand_login_view from .utils import login_url as make_login_url from .utils import make_next_param class LoginManager: """This object is used to hold the settings used for logging in. Instances of :class:`LoginManager` are *not* bound to specific apps, so you can create one in the main body of your code and then bind it to your app in a factory function. """ def __init__(self, app=None, add_context_processor=True): #: A class or factory function that produces an anonymous user, which #: is used when no one is logged in. self.anonymous_user = AnonymousUserMixin #: The name of the view to redirect to when the user needs to log in. #: (This can be an absolute URL as well, if your authentication #: machinery is external to your application.) self.login_view = None #: Names of views to redirect to when the user needs to log in, #: per blueprint. If the key value is set to None the value of #: :attr:`login_view` will be used instead. self.blueprint_login_views = {} #: The message to flash when a user is redirected to the login page. self.login_message = LOGIN_MESSAGE #: The message category to flash when a user is redirected to the login #: page. self.login_message_category = LOGIN_MESSAGE_CATEGORY #: The name of the view to redirect to when the user needs to #: reauthenticate. self.refresh_view = None #: The message to flash when a user is redirected to the 'needs #: refresh' page. self.needs_refresh_message = REFRESH_MESSAGE #: The message category to flash when a user is redirected to the #: 'needs refresh' page. self.needs_refresh_message_category = REFRESH_MESSAGE_CATEGORY #: The mode to use session protection in. This can be either #: ``'basic'`` (the default) or ``'strong'``, or ``None`` to disable #: it. self.session_protection = "basic" #: If present, used to translate flash messages ``self.login_message`` #: and ``self.needs_refresh_message`` self.localize_callback = None self.unauthorized_callback = None self.needs_refresh_callback = None self.id_attribute = ID_ATTRIBUTE self._user_callback = None self._header_callback = None self._request_callback = None self._session_identifier_generator = _create_identifier if app is not None: self.init_app(app, add_context_processor) def setup_app(self, app, add_context_processor=True): # pragma: no cover """ This method has been deprecated. Please use :meth:`LoginManager.init_app` instead. """ import warnings warnings.warn( "'setup_app' is deprecated and will be removed in" " Flask-Login 0.7. Use 'init_app' instead.", DeprecationWarning, stacklevel=2, ) self.init_app(app, add_context_processor) def init_app(self, app, add_context_processor=True): """ Configures an application. This registers an `after_request` call, and attaches this `LoginManager` to it as `app.login_manager`. :param app: The :class:`flask.Flask` object to configure. :type app: :class:`flask.Flask` :param add_context_processor: Whether to add a context processor to the app that adds a `current_user` variable to the template. Defaults to ``True``. :type add_context_processor: bool """ app.login_manager = self app.after_request(self._update_remember_cookie) if add_context_processor: app.context_processor(_user_context_processor) def unauthorized(self): """ This is called when the user is required to log in. If you register a callback with :meth:`LoginManager.unauthorized_handler`, then it will be called. Otherwise, it will take the following actions: - Flash :attr:`LoginManager.login_message` to the user. - If the app is using blueprints find the login view for the current blueprint using `blueprint_login_views`. If the app is not using blueprints or the login view for the current blueprint is not specified use the value of `login_view`. - Redirect the user to the login view. (The page they were attempting to access will be passed in the ``next`` query string variable, so you can redirect there if present instead of the homepage. Alternatively, it will be added to the session as ``next`` if USE_SESSION_FOR_NEXT is set.) If :attr:`LoginManager.login_view` is not defined, then it will simply raise a HTTP 401 (Unauthorized) error instead. This should be returned from a view or before/after_request function, otherwise the redirect will have no effect. """ user_unauthorized.send(current_app._get_current_object()) if self.unauthorized_callback: return self.unauthorized_callback() if request.blueprint in self.blueprint_login_views: login_view = self.blueprint_login_views[request.blueprint] else: login_view = self.login_view if not login_view: abort(401) if self.login_message: if self.localize_callback is not None: flash( self.localize_callback(self.login_message), category=self.login_message_category, ) else: flash(self.login_message, category=self.login_message_category) config = current_app.config if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT): login_url = expand_login_view(login_view) session["_id"] = self._session_identifier_generator() session["next"] = make_next_param(login_url, request.url) redirect_url = make_login_url(login_view) else: redirect_url = make_login_url(login_view, next_url=request.url) return redirect(redirect_url) def user_loader(self, callback): """ This sets the callback for reloading a user from the session. The function you set should take a user ID (a ``str``) and return a user object, or ``None`` if the user does not exist. :param callback: The callback for retrieving a user object. :type callback: callable """ self._user_callback = callback return self.user_callback @property def user_callback(self): """Gets the user_loader callback set by user_loader decorator.""" return self._user_callback def request_loader(self, callback): """ This sets the callback for loading a user from a Flask request. The function you set should take Flask request object and return a user object, or `None` if the user does not exist. :param callback: The callback for retrieving a user object. :type callback: callable """ self._request_callback = callback return self.request_callback @property def request_callback(self): """Gets the request_loader callback set by request_loader decorator.""" return self._request_callback def unauthorized_handler(self, callback): """ This will set the callback for the `unauthorized` method, which among other things is used by `login_required`. It takes no arguments, and should return a response to be sent to the user instead of their normal view. :param callback: The callback for unauthorized users. :type callback: callable """ self.unauthorized_callback = callback return callback def needs_refresh_handler(self, callback): """ This will set the callback for the `needs_refresh` method, which among other things is used by `fresh_login_required`. It takes no arguments, and should return a response to be sent to the user instead of their normal view. :param callback: The callback for unauthorized users. :type callback: callable """ self.needs_refresh_callback = callback return callback def needs_refresh(self): """ This is called when the user is logged in, but they need to be reauthenticated because their session is stale. If you register a callback with `needs_refresh_handler`, then it will be called. Otherwise, it will take the following actions: - Flash :attr:`LoginManager.needs_refresh_message` to the user. - Redirect the user to :attr:`LoginManager.refresh_view`. (The page they were attempting to access will be passed in the ``next`` query string variable, so you can redirect there if present instead of the homepage.) If :attr:`LoginManager.refresh_view` is not defined, then it will simply raise a HTTP 401 (Unauthorized) error instead. This should be returned from a view or before/after_request function, otherwise the redirect will have no effect. """ user_needs_refresh.send(current_app._get_current_object()) if self.needs_refresh_callback: return self.needs_refresh_callback() if not self.refresh_view: abort(401) if self.needs_refresh_message: if self.localize_callback is not None: flash( self.localize_callback(self.needs_refresh_message), category=self.needs_refresh_message_category, ) else: flash( self.needs_refresh_message, category=self.needs_refresh_message_category, ) config = current_app.config if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT): login_url = expand_login_view(self.refresh_view) session["_id"] = self._session_identifier_generator() session["next"] = make_next_param(login_url, request.url) redirect_url = make_login_url(self.refresh_view) else: login_url = self.refresh_view redirect_url = make_login_url(login_url, next_url=request.url) return redirect(redirect_url) def header_loader(self, callback): """ This function has been deprecated. Please use :meth:`LoginManager.request_loader` instead. This sets the callback for loading a user from a header value. The function you set should take an authentication token and return a user object, or `None` if the user does not exist. :param callback: The callback for retrieving a user object. :type callback: callable """ import warnings warnings.warn( "'header_loader' is deprecated and will be removed in" " Flask-Login 0.7. Use 'request_loader' instead.", DeprecationWarning, stacklevel=2, ) self._header_callback = callback return callback def _update_request_context_with_user(self, user=None): """Store the given user as ctx.user.""" if user is None: user = self.anonymous_user() g._login_user = user def _load_user(self): """Loads user from session or remember_me cookie as applicable""" if self._user_callback is None and self._request_callback is None: raise Exception( "Missing user_loader or request_loader. Refer to " "http://flask-login.readthedocs.io/#how-it-works " "for more info." ) user_accessed.send(current_app._get_current_object()) # Check SESSION_PROTECTION if self._session_protection_failed(): return self._update_request_context_with_user() user = None # Load user from Flask Session user_id = session.get("_user_id") if user_id is not None and self._user_callback is not None: user = self._user_callback(user_id) # Load user from Remember Me Cookie or Request Loader if user is None: config = current_app.config cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME) header_name = config.get("AUTH_HEADER_NAME", AUTH_HEADER_NAME) has_cookie = ( cookie_name in request.cookies and session.get("_remember") != "clear" ) if has_cookie: cookie = request.cookies[cookie_name] user = self._load_user_from_remember_cookie(cookie) elif self._request_callback: user = self._load_user_from_request(request) elif header_name in request.headers: header = request.headers[header_name] user = self._load_user_from_header(header) return self._update_request_context_with_user(user) def _session_protection_failed(self): sess = session._get_current_object() ident = self._session_identifier_generator() app = current_app._get_current_object() mode = app.config.get("SESSION_PROTECTION", self.session_protection) if not mode or mode not in ["basic", "strong"]: return False # if the sess is empty, it's an anonymous user or just logged out # so we can skip this if sess and ident != sess.get("_id", None): if mode == "basic" or sess.permanent: if sess.get("_fresh") is not False: sess["_fresh"] = False session_protected.send(app) return False elif mode == "strong": for k in SESSION_KEYS: sess.pop(k, None) sess["_remember"] = "clear" session_protected.send(app) return True return False def _load_user_from_remember_cookie(self, cookie): user_id = decode_cookie(cookie) if user_id is not None: session["_user_id"] = user_id session["_fresh"] = False user = None if self._user_callback: user = self._user_callback(user_id) if user is not None: app = current_app._get_current_object() user_loaded_from_cookie.send(app, user=user) return user return None def _load_user_from_header(self, header): if self._header_callback: user = self._header_callback(header) if user is not None: app = current_app._get_current_object() from .signals import _user_loaded_from_header _user_loaded_from_header.send(app, user=user) return user return None def _load_user_from_request(self, request): if self._request_callback: user = self._request_callback(request) if user is not None: app = current_app._get_current_object() user_loaded_from_request.send(app, user=user) return user return None def _update_remember_cookie(self, response): # Don't modify the session unless there's something to do. if "_remember" not in session and current_app.config.get( "REMEMBER_COOKIE_REFRESH_EACH_REQUEST" ): session["_remember"] = "set" if "_remember" in session: operation = session.pop("_remember", None) if operation == "set" and "_user_id" in session: self._set_cookie(response) elif operation == "clear": self._clear_cookie(response) return response def _set_cookie(self, response): # cookie settings config = current_app.config cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME) domain = config.get("REMEMBER_COOKIE_DOMAIN") path = config.get("REMEMBER_COOKIE_PATH", "/") secure = config.get("REMEMBER_COOKIE_SECURE", COOKIE_SECURE) httponly = config.get("REMEMBER_COOKIE_HTTPONLY", COOKIE_HTTPONLY) samesite = config.get("REMEMBER_COOKIE_SAMESITE", COOKIE_SAMESITE) if "_remember_seconds" in session: duration = timedelta(seconds=session["_remember_seconds"]) else: duration = config.get("REMEMBER_COOKIE_DURATION", COOKIE_DURATION) # prepare data data = encode_cookie(str(session["_user_id"])) if isinstance(duration, int): duration = timedelta(seconds=duration) try: expires = datetime.utcnow() + duration except TypeError as e: raise Exception( "REMEMBER_COOKIE_DURATION must be a datetime.timedelta," f" instead got: {duration}" ) from e # actually set it response.set_cookie( cookie_name, value=data, expires=expires, domain=domain, path=path, secure=secure, httponly=httponly, samesite=samesite, ) def _clear_cookie(self, response): config = current_app.config cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME) domain = config.get("REMEMBER_COOKIE_DOMAIN") path = config.get("REMEMBER_COOKIE_PATH", "/") response.delete_cookie(cookie_name, domain=domain, path=path) @property def _login_disabled(self): """Legacy property, use app.config['LOGIN_DISABLED'] instead.""" import warnings warnings.warn( "'_login_disabled' is deprecated and will be removed in" " Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'" " instead.", DeprecationWarning, stacklevel=2, ) if has_app_context(): return current_app.config.get("LOGIN_DISABLED", False) return False @_login_disabled.setter def _login_disabled(self, newvalue): """Legacy property setter, use app.config['LOGIN_DISABLED'] instead.""" import warnings warnings.warn( "'_login_disabled' is deprecated and will be removed in" " Flask-Login 0.7. Use 'LOGIN_DISABLED' in 'app.config'" " instead.", DeprecationWarning, stacklevel=2, ) current_app.config["LOGIN_DISABLED"] = newvalue flask-login-0.6.3/src/flask_login/mixins.py000066400000000000000000000027701451774104400207030ustar00rootroot00000000000000class UserMixin: """ This provides default implementations for the methods that Flask-Login expects user objects to have. """ # Python 3 implicitly set __hash__ to None if we override __eq__ # We set it back to its default implementation __hash__ = object.__hash__ @property def is_active(self): return True @property def is_authenticated(self): return self.is_active @property def is_anonymous(self): return False def get_id(self): try: return str(self.id) except AttributeError: raise NotImplementedError("No `id` attribute - override `get_id`") from None def __eq__(self, other): """ Checks the equality of two `UserMixin` objects using `get_id`. """ if isinstance(other, UserMixin): return self.get_id() == other.get_id() return NotImplemented def __ne__(self, other): """ Checks the inequality of two `UserMixin` objects using `get_id`. """ equal = self.__eq__(other) if equal is NotImplemented: return NotImplemented return not equal class AnonymousUserMixin: """ This is the default object for representing an anonymous user. """ @property def is_authenticated(self): return False @property def is_active(self): return False @property def is_anonymous(self): return True def get_id(self): return flask-login-0.6.3/src/flask_login/signals.py000066400000000000000000000046401451774104400210320ustar00rootroot00000000000000from flask.signals import Namespace _signals = Namespace() #: Sent when a user is logged in. In addition to the app (which is the #: sender), it is passed `user`, which is the user being logged in. user_logged_in = _signals.signal("logged-in") #: Sent when a user is logged out. In addition to the app (which is the #: sender), it is passed `user`, which is the user being logged out. user_logged_out = _signals.signal("logged-out") #: Sent when the user is loaded from the cookie. In addition to the app (which #: is the sender), it is passed `user`, which is the user being reloaded. user_loaded_from_cookie = _signals.signal("loaded-from-cookie") #: Sent when the user is loaded from the header. In addition to the app (which #: is the #: sender), it is passed `user`, which is the user being reloaded. _user_loaded_from_header = _signals.signal("loaded-from-header") #: Sent when the user is loaded from the request. In addition to the app (which #: is the #: sender), it is passed `user`, which is the user being reloaded. user_loaded_from_request = _signals.signal("loaded-from-request") #: Sent when a user's login is confirmed, marking it as fresh. (It is not #: called for a normal login.) #: It receives no additional arguments besides the app. user_login_confirmed = _signals.signal("login-confirmed") #: Sent when the `unauthorized` method is called on a `LoginManager`. It #: receives no additional arguments besides the app. user_unauthorized = _signals.signal("unauthorized") #: Sent when the `needs_refresh` method is called on a `LoginManager`. It #: receives no additional arguments besides the app. user_needs_refresh = _signals.signal("needs-refresh") #: Sent whenever the user is accessed/loaded #: receives no additional arguments besides the app. user_accessed = _signals.signal("accessed") #: Sent whenever session protection takes effect, and a session is either #: marked non-fresh or deleted. It receives no additional arguments besides #: the app. session_protected = _signals.signal("session-protected") def __getattr__(name): if name == "user_loaded_from_header": import warnings warnings.warn( "'user_loaded_from_header' is deprecated and will be" " removed in Flask-Login 0.7. Use" " 'user_loaded_from_request' instead.", DeprecationWarning, stacklevel=2, ) return _user_loaded_from_header raise AttributeError(name) flask-login-0.6.3/src/flask_login/test_client.py000066400000000000000000000010051451774104400216770ustar00rootroot00000000000000from flask.testing import FlaskClient class FlaskLoginClient(FlaskClient): """ A Flask test client that knows how to log in users using the Flask-Login extension. """ def __init__(self, *args, **kwargs): user = kwargs.pop("user", None) fresh = kwargs.pop("fresh_login", True) super().__init__(*args, **kwargs) if user: with self.session_transaction() as sess: sess["_user_id"] = user.get_id() sess["_fresh"] = fresh flask-login-0.6.3/src/flask_login/utils.py000066400000000000000000000333051451774104400205320ustar00rootroot00000000000000import hmac from functools import wraps from hashlib import sha512 from urllib.parse import parse_qs from urllib.parse import urlencode from urllib.parse import urlsplit from urllib.parse import urlunsplit from flask import current_app from flask import g from flask import has_request_context from flask import request from flask import session from flask import url_for from werkzeug.local import LocalProxy from .config import COOKIE_NAME from .config import EXEMPT_METHODS from .signals import user_logged_in from .signals import user_logged_out from .signals import user_login_confirmed #: A proxy for the current user. If no user is logged in, this will be an #: anonymous user current_user = LocalProxy(lambda: _get_user()) def encode_cookie(payload, key=None): """ This will encode a ``str`` value into a cookie, and sign that cookie with the app's secret key. :param payload: The value to encode, as `str`. :type payload: str :param key: The key to use when creating the cookie digest. If not specified, the SECRET_KEY value from app config will be used. :type key: str """ return f"{payload}|{_cookie_digest(payload, key=key)}" def decode_cookie(cookie, key=None): """ This decodes a cookie given by `encode_cookie`. If verification of the cookie fails, ``None`` will be implicitly returned. :param cookie: An encoded cookie. :type cookie: str :param key: The key to use when creating the cookie digest. If not specified, the SECRET_KEY value from app config will be used. :type key: str """ try: payload, digest = cookie.rsplit("|", 1) if hasattr(digest, "decode"): digest = digest.decode("ascii") # pragma: no cover except ValueError: return if hmac.compare_digest(_cookie_digest(payload, key=key), digest): return payload def make_next_param(login_url, current_url): """ Reduces the scheme and host from a given URL so it can be passed to the given `login` URL more efficiently. :param login_url: The login URL being redirected to. :type login_url: str :param current_url: The URL to reduce. :type current_url: str """ l_url = urlsplit(login_url) c_url = urlsplit(current_url) if (not l_url.scheme or l_url.scheme == c_url.scheme) and ( not l_url.netloc or l_url.netloc == c_url.netloc ): return urlunsplit(("", "", c_url.path, c_url.query, "")) return current_url def expand_login_view(login_view): """ Returns the url for the login view, expanding the view name to a url if needed. :param login_view: The name of the login view or a URL for the login view. :type login_view: str """ if login_view.startswith(("https://", "http://", "/")): return login_view return url_for(login_view) def login_url(login_view, next_url=None, next_field="next"): """ Creates a URL for redirecting to a login page. If only `login_view` is provided, this will just return the URL for it. If `next_url` is provided, however, this will append a ``next=URL`` parameter to the query string so that the login view can redirect back to that URL. Flask-Login's default unauthorized handler uses this function when redirecting to your login url. To force the host name used, set `FORCE_HOST_FOR_REDIRECTS` to a host. This prevents from redirecting to external sites if request headers Host or X-Forwarded-For are present. :param login_view: The name of the login view. (Alternately, the actual URL to the login view.) :type login_view: str :param next_url: The URL to give the login view for redirection. :type next_url: str :param next_field: What field to store the next URL in. (It defaults to ``next``.) :type next_field: str """ base = expand_login_view(login_view) if next_url is None: return base parsed_result = urlsplit(base) md = parse_qs(parsed_result.query, keep_blank_values=True) md[next_field] = make_next_param(base, next_url) netloc = current_app.config.get("FORCE_HOST_FOR_REDIRECTS") or parsed_result.netloc parsed_result = parsed_result._replace( netloc=netloc, query=urlencode(md, doseq=True) ) return urlunsplit(parsed_result) def login_fresh(): """ This returns ``True`` if the current login is fresh. """ return session.get("_fresh", False) def login_remembered(): """ This returns ``True`` if the current login is remembered across sessions. """ config = current_app.config cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME) has_cookie = cookie_name in request.cookies and session.get("_remember") != "clear" if has_cookie: cookie = request.cookies[cookie_name] user_id = decode_cookie(cookie) return user_id is not None return False def login_user(user, remember=False, duration=None, force=False, fresh=True): """ Logs a user in. You should pass the actual user object to this. If the user's `is_active` property is ``False``, they will not be logged in unless `force` is ``True``. This will return ``True`` if the log in attempt succeeds, and ``False`` if it fails (i.e. because the user is inactive). :param user: The user object to log in. :type user: object :param remember: Whether to remember the user after their session expires. Defaults to ``False``. :type remember: bool :param duration: The amount of time before the remember cookie expires. If ``None`` the value set in the settings is used. Defaults to ``None``. :type duration: :class:`datetime.timedelta` :param force: If the user is inactive, setting this to ``True`` will log them in regardless. Defaults to ``False``. :type force: bool :param fresh: setting this to ``False`` will log in the user with a session marked as not "fresh". Defaults to ``True``. :type fresh: bool """ if not force and not user.is_active: return False user_id = getattr(user, current_app.login_manager.id_attribute)() session["_user_id"] = user_id session["_fresh"] = fresh session["_id"] = current_app.login_manager._session_identifier_generator() if remember: session["_remember"] = "set" if duration is not None: try: # equal to timedelta.total_seconds() but works with Python 2.6 session["_remember_seconds"] = ( duration.microseconds + (duration.seconds + duration.days * 24 * 3600) * 10**6 ) / 10.0**6 except AttributeError as e: raise Exception( f"duration must be a datetime.timedelta, instead got: {duration}" ) from e current_app.login_manager._update_request_context_with_user(user) user_logged_in.send(current_app._get_current_object(), user=_get_user()) return True def logout_user(): """ Logs a user out. (You do not need to pass the actual user.) This will also clean up the remember me cookie if it exists. """ user = _get_user() if "_user_id" in session: session.pop("_user_id") if "_fresh" in session: session.pop("_fresh") if "_id" in session: session.pop("_id") cookie_name = current_app.config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME) if cookie_name in request.cookies: session["_remember"] = "clear" if "_remember_seconds" in session: session.pop("_remember_seconds") user_logged_out.send(current_app._get_current_object(), user=user) current_app.login_manager._update_request_context_with_user() return True def confirm_login(): """ This sets the current session as fresh. Sessions become stale when they are reloaded from a cookie. """ session["_fresh"] = True session["_id"] = current_app.login_manager._session_identifier_generator() user_login_confirmed.send(current_app._get_current_object()) def login_required(func): """ If you decorate a view with this, it will ensure that the current user is logged in and authenticated before calling the actual view. (If they are not, it calls the :attr:`LoginManager.unauthorized` callback.) For example:: @app.route('/post') @login_required def post(): pass If there are only certain times you need to require that your user is logged in, you can do so with:: if not current_user.is_authenticated: return current_app.login_manager.unauthorized() ...which is essentially the code that this function adds to your views. It can be convenient to globally turn off authentication when unit testing. To enable this, if the application configuration variable `LOGIN_DISABLED` is set to `True`, this decorator will be ignored. .. Note :: Per `W3 guidelines for CORS preflight requests `_, HTTP ``OPTIONS`` requests are exempt from login checks. :param func: The view function to decorate. :type func: function """ @wraps(func) def decorated_view(*args, **kwargs): if request.method in EXEMPT_METHODS or current_app.config.get("LOGIN_DISABLED"): pass elif not current_user.is_authenticated: return current_app.login_manager.unauthorized() # flask 1.x compatibility # current_app.ensure_sync is only available in Flask >= 2.0 if callable(getattr(current_app, "ensure_sync", None)): return current_app.ensure_sync(func)(*args, **kwargs) return func(*args, **kwargs) return decorated_view def fresh_login_required(func): """ If you decorate a view with this, it will ensure that the current user's login is fresh - i.e. their session was not restored from a 'remember me' cookie. Sensitive operations, like changing a password or e-mail, should be protected with this, to impede the efforts of cookie thieves. If the user is not authenticated, :meth:`LoginManager.unauthorized` is called as normal. If they are authenticated, but their session is not fresh, it will call :meth:`LoginManager.needs_refresh` instead. (In that case, you will need to provide a :attr:`LoginManager.refresh_view`.) Behaves identically to the :func:`login_required` decorator with respect to configuration variables. .. Note :: Per `W3 guidelines for CORS preflight requests `_, HTTP ``OPTIONS`` requests are exempt from login checks. :param func: The view function to decorate. :type func: function """ @wraps(func) def decorated_view(*args, **kwargs): if request.method in EXEMPT_METHODS or current_app.config.get("LOGIN_DISABLED"): pass elif not current_user.is_authenticated: return current_app.login_manager.unauthorized() elif not login_fresh(): return current_app.login_manager.needs_refresh() try: # current_app.ensure_sync available in Flask >= 2.0 return current_app.ensure_sync(func)(*args, **kwargs) except AttributeError: # pragma: no cover return func(*args, **kwargs) return decorated_view def set_login_view(login_view, blueprint=None): """ Sets the login view for the app or blueprint. If a blueprint is passed, the login view is set for this blueprint on ``blueprint_login_views``. :param login_view: The user object to log in. :type login_view: str :param blueprint: The blueprint which this login view should be set on. Defaults to ``None``. :type blueprint: object """ num_login_views = len(current_app.login_manager.blueprint_login_views) if blueprint is not None or num_login_views != 0: (current_app.login_manager.blueprint_login_views[blueprint.name]) = login_view if ( current_app.login_manager.login_view is not None and None not in current_app.login_manager.blueprint_login_views ): ( current_app.login_manager.blueprint_login_views[None] ) = current_app.login_manager.login_view current_app.login_manager.login_view = None else: current_app.login_manager.login_view = login_view def _get_user(): if has_request_context(): if "_login_user" not in g: current_app.login_manager._load_user() return g._login_user return None def _cookie_digest(payload, key=None): key = _secret_key(key) return hmac.new(key, payload.encode("utf-8"), sha512).hexdigest() def _get_remote_addr(): address = request.headers.get("X-Forwarded-For", request.remote_addr) if address is not None: # An 'X-Forwarded-For' header includes a comma separated list of the # addresses, the first address being the actual remote address. address = address.encode("utf-8").split(b",")[0].strip() return address def _create_identifier(): user_agent = request.headers.get("User-Agent") if user_agent is not None: user_agent = user_agent.encode("utf-8") base = f"{_get_remote_addr()}|{user_agent}" if str is bytes: base = str(base, "utf-8", errors="replace") # pragma: no cover h = sha512() h.update(base.encode("utf8")) return h.hexdigest() def _user_context_processor(): return dict(current_user=_get_user()) def _secret_key(key=None): if key is None: key = current_app.config["SECRET_KEY"] if isinstance(key, str): # pragma: no cover key = key.encode("latin1") # ensure bytes return key flask-login-0.6.3/tests/000077500000000000000000000000001451774104400150775ustar00rootroot00000000000000flask-login-0.6.3/tests/test_login.py000066400000000000000000002057771451774104400176420ustar00rootroot00000000000000from __future__ import annotations import sys import unittest from collections.abc import Hashable from contextlib import contextmanager from dataclasses import dataclass from datetime import datetime from datetime import timedelta from datetime import timezone from unittest.mock import ANY from unittest.mock import Mock from unittest.mock import patch from flask import Blueprint from flask import Flask from flask import get_flashed_messages from flask import Response from flask import session from flask.views import MethodView from flask_login import AnonymousUserMixin from flask_login import confirm_login from flask_login import current_user from flask_login import decode_cookie from flask_login import encode_cookie from flask_login import FlaskLoginClient from flask_login import fresh_login_required from flask_login import login_fresh from flask_login import login_remembered from flask_login import login_required from flask_login import login_url from flask_login import login_user from flask_login import LoginManager from flask_login import logout_user from flask_login import make_next_param from flask_login import session_protected from flask_login import set_login_view from flask_login import user_accessed from flask_login import user_loaded_from_cookie from flask_login import user_loaded_from_request from flask_login import user_logged_in from flask_login import user_logged_out from flask_login import user_login_confirmed from flask_login import user_needs_refresh from flask_login import user_unauthorized from flask_login import UserMixin from flask_login.__about__ import __author__ from flask_login.__about__ import __author_email__ from flask_login.__about__ import __copyright__ from flask_login.__about__ import __description__ from flask_login.__about__ import __license__ from flask_login.__about__ import __maintainer__ from flask_login.__about__ import __title__ from flask_login.__about__ import __url__ from flask_login.__about__ import __version__ from flask_login.__about__ import __version_info__ from flask_login.utils import _secret_key from flask_login.utils import _user_context_processor from semantic_version import Version from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.test import Client sys_version = Version( major=sys.version_info.major, minor=sys.version_info.minor, patch=sys.version_info.micro, ) # Support Werkzeug < 2.3 new_cookie_methods = hasattr(Client, "get_cookie") @dataclass class BasicCookie: key: str value: str expires: datetime | None def client_get_cookie( client: Client, key: str, domain: str = "localhost", path: str = "/" ) -> BasicCookie | None: if new_cookie_methods: if domain.startswith("."): domain = domain[1:] cookie = client.get_cookie(key, domain, path) if cookie is None: return None return BasicCookie(cookie.key, cookie.value, cookie.expires) else: try: cookie = client.cookie_jar._cookies[domain][path][key] if cookie.expires is None: expires = None else: expires = datetime.fromtimestamp(cookie.expires, timezone.utc) return BasicCookie(cookie.name, cookie.value, expires) except KeyError: return None def client_set_cookie( client: Client, key: str, value: str, domain: str | None = None ) -> None: if new_cookie_methods: if domain.startswith("."): domain = domain[1:] client.set_cookie(key, value, domain=domain) else: client.set_cookie(domain, key, value) @contextmanager def listen_to(signal): """Context Manager that listens to signals and records emissions Example: with listen_to(user_logged_in) as listener: login_user(user) # Assert that a single emittance of the specific args was seen. listener.assert_heard_one(app, user=user)) # Of course, you can always just look at the list yourself self.assertEqual(1, len(listener.heard)) """ class _SignalsCaught: def __init__(self): self.heard = [] def add(self, *args, **kwargs): """The actual handler of the signal.""" self.heard.append((args, kwargs)) def assert_heard_one(self, *args, **kwargs): """The signal fired once, and with the arguments given""" if len(self.heard) == 0: raise AssertionError("No signals were fired") elif len(self.heard) > 1: msg = f"{len(self.heard)} signals were fired" raise AssertionError(msg) elif self.heard[0] != (args, kwargs): raise AssertionError( "One signal was heard, but with incorrect" f" arguments: Got ({self.heard[0]}) expected" f" ({args}, {kwargs})" ) def assert_heard_none(self, *args, **kwargs): """The signal fired no times""" if len(self.heard) >= 1: msg = f"{len(self.heard)} signals were fired" raise AssertionError(msg) results = _SignalsCaught() signal.connect(results.add) try: yield results finally: signal.disconnect(results.add) class User(UserMixin): def __init__(self, name, id, active=True): self.id = id self.name = name self.active = active def get_id(self): return self.id @property def is_active(self): return self.active notch = User("Notch", 1) steve = User("Steve", 2) creeper = User("Creeper", 3, False) germanjapanese = User("Müller", "佐藤") # str user_id USERS = {1: notch, 2: steve, 3: creeper, "佐藤": germanjapanese} class AboutTestCase(unittest.TestCase): """Make sure we can get version and other info.""" def test_have_about_data(self): self.assertTrue(__title__ is not None) self.assertTrue(__description__ is not None) self.assertTrue(__url__ is not None) self.assertTrue(__version_info__ is not None) self.assertTrue(__version__ is not None) self.assertTrue(__author__ is not None) self.assertTrue(__author_email__ is not None) self.assertTrue(__maintainer__ is not None) self.assertTrue(__license__ is not None) self.assertTrue(__copyright__ is not None) class StaticTestCase(unittest.TestCase): def test_static_loads_anonymous(self): app = Flask(__name__) app.static_url_path = "/static" app.secret_key = "this is a temp key" lm = LoginManager() lm.init_app(app) @lm.user_loader def load_user(user_id): return USERS[int(user_id)] with app.test_client() as c: c.get("/static/favicon.ico") self.assertTrue(current_user.is_anonymous) def test_static_loads_without_accessing_session(self): app = Flask(__name__) app.static_url_path = "/static" app.secret_key = "this is a temp key" lm = LoginManager() lm.init_app(app) @lm.user_loader def load_user(user_id): return USERS[int(user_id)] with app.test_client() as c: with listen_to(user_accessed) as listener: c.get("/static/favicon.ico") listener.assert_heard_none(app) class InitializationTestCase(unittest.TestCase): """Tests the two initialization methods""" def setUp(self): self.app = Flask(__name__) self.app.config["SECRET_KEY"] = "1234" def test_init_app(self): login_manager = LoginManager() login_manager.init_app(self.app, add_context_processor=True) self.assertIsInstance(login_manager, LoginManager) def test_class_init(self): login_manager = LoginManager(self.app, add_context_processor=True) self.assertIsInstance(login_manager, LoginManager) def test_no_user_loader_raises(self): login_manager = LoginManager(self.app, add_context_processor=True) with self.app.test_request_context(): session["_user_id"] = "2" with self.assertRaises(Exception) as cm: login_manager._load_user() expected_message = "Missing user_loader or request_loader" self.assertTrue(str(cm.exception).startswith(expected_message)) class MethodViewLoginTestCase(unittest.TestCase): def setUp(self): self.app = Flask(__name__) self.login_manager = LoginManager() self.login_manager.init_app(self.app) self.app.config["LOGIN_DISABLED"] = False class SecretEndpoint(MethodView): decorators = [ login_required, fresh_login_required, ] def options(self): return "" def get(self): return "" self.app.add_url_rule("/secret", view_func=SecretEndpoint.as_view("secret")) def test_options_call_exempt(self): with self.app.test_client() as c: result = c.open("/secret", method="OPTIONS") self.assertEqual(result.status_code, 200) class LoginTestCase(unittest.TestCase): """Tests for results of the login_user function""" def setUp(self): self.app = Flask(__name__) self.app.config["SECRET_KEY"] = "deterministic" self.app.config["SESSION_PROTECTION"] = None self.remember_cookie_name = "remember" self.app.config["REMEMBER_COOKIE_NAME"] = self.remember_cookie_name self.login_manager = LoginManager() self.login_manager.init_app(self.app) self.app.config["LOGIN_DISABLED"] = False # Disable absolute location, like Werkzeug 2.1 self.app.response_class.autocorrect_location_header = False @self.app.route("/") def index(): return "Welcome!" @self.app.route("/secret") def secret(): return self.login_manager.unauthorized() @self.app.route("/login-notch") def login_notch(): return str(login_user(notch)) @self.app.route("/login-notch-remember") def login_notch_remember(): return str(login_user(notch, remember=True)) @self.app.route("/login-notch-remember-custom") def login_notch_remember_custom(): duration = timedelta(hours=7) return str(login_user(notch, remember=True, duration=duration)) @self.app.route("/login-notch-permanent") def login_notch_permanent(): session.permanent = True return str(login_user(notch)) @self.app.route("/needs-refresh") def needs_refresh(): return self.login_manager.needs_refresh() @self.app.route("/confirm-login") def _confirm_login(): confirm_login() return "" @self.app.route("/username") def username(): if current_user.is_authenticated: return current_user.name return "Anonymous" @self.app.route("/is-fresh") def is_fresh(): return str(login_fresh()) @self.app.route("/is-remembered") def is_remembered(): return str(login_remembered()) @self.app.route("/logout") def logout(): return str(logout_user()) @self.login_manager.user_loader def load_user(user_id): return USERS[int(user_id)] @self.login_manager.request_loader def load_user_from_request(request): user_id = request.args.get("user_id") try: user_id = int(float(user_id)) except TypeError: pass return USERS.get(user_id) @self.app.route("/empty_session") def empty_session(): return f"modified={session.modified}" # This will help us with the possibility of typoes in the tests. Now # we shouldn't have to check each response to help us set up state # (such as login pages) to make sure it worked: we will always # get an exception raised (rather than return a 404 response) @self.app.errorhandler(404) def handle_404(e): raise e unittest.TestCase.setUp(self) def _delete_session(self, c): # Helper method to cause the session to be deleted # as if the browser was closed. This will remove # the session regardless of the permanent flag # on the session! with c.session_transaction() as sess: sess.clear() # # Login # def test_test_request_context_users_are_anonymous(self): with self.app.test_request_context(): self.assertTrue(current_user.is_anonymous) def test_defaults_anonymous(self): with self.app.test_client() as c: result = c.get("/username") self.assertEqual("Anonymous", result.data.decode("utf-8")) def test_login_user(self): with self.app.test_request_context(): result = login_user(notch) self.assertTrue(result) self.assertEqual(current_user.name, "Notch") self.assertIs(login_fresh(), True) def test_login_user_not_fresh(self): with self.app.test_request_context(): result = login_user(notch, fresh=False) self.assertTrue(result) self.assertEqual(current_user.name, "Notch") self.assertIs(login_fresh(), False) def test_login_user_emits_signal(self): with self.app.test_request_context(): with listen_to(user_logged_in) as listener: login_user(notch) listener.assert_heard_one(self.app, user=notch) def test_login_inactive_user(self): with self.app.test_request_context(): result = login_user(creeper) self.assertTrue(current_user.is_anonymous) self.assertFalse(result) def test_login_inactive_user_forced(self): with self.app.test_request_context(): login_user(creeper, force=True) self.assertEqual(current_user.name, "Creeper") def test_login_user_with_request(self): user_id = 2 user_name = USERS[user_id].name with self.app.test_client() as c: url = f"/username?user_id={user_id}" result = c.get(url) self.assertEqual(user_name, result.data.decode("utf-8")) def test_login_invalid_user_with_request(self): user_id = 9000 user_name = "Anonymous" with self.app.test_client() as c: url = f"/username?user_id={user_id}" result = c.get(url) self.assertEqual(user_name, result.data.decode("utf-8")) # # Logout # def test_logout_logs_out_current_user(self): with self.app.test_request_context(): login_user(notch) logout_user() self.assertTrue(current_user.is_anonymous) def test_logout_emits_signal(self): with self.app.test_request_context(): login_user(notch) with listen_to(user_logged_out) as listener: logout_user() listener.assert_heard_one(self.app, user=notch) def test_logout_without_current_user(self): with self.app.test_request_context(): login_user(notch) del session["_user_id"] with listen_to(user_logged_out) as listener: logout_user() listener.assert_heard_one(self.app, user=ANY) # # Unauthorized # def test_unauthorized_fires_unauthorized_signal(self): with self.app.test_client() as c: with listen_to(user_unauthorized) as listener: c.get("/secret") listener.assert_heard_one(self.app) def test_unauthorized_flashes_message_with_login_view(self): self.login_manager.login_view = "/login" expected_message = self.login_manager.login_message = "Log in!" expected_category = self.login_manager.login_message_category = "login" with self.app.test_client() as c: c.get("/secret") msgs = get_flashed_messages(category_filter=[expected_category]) self.assertEqual([expected_message], msgs) def test_unauthorized_flash_message_localized(self): def _gettext(msg): if msg == "Log in!": return "Einloggen" self.login_manager.login_view = "/login" self.login_manager.localize_callback = _gettext self.login_manager.login_message = "Log in!" expected_message = "Einloggen" expected_category = self.login_manager.login_message_category = "login" with self.app.test_client() as c: c.get("/secret") msgs = get_flashed_messages(category_filter=[expected_category]) self.assertEqual([expected_message], msgs) self.login_manager.localize_callback = None def test_unauthorized_uses_authorized_handler(self): @self.login_manager.unauthorized_handler def _callback(): return Response("This is secret!", 401) with self.app.test_client() as c: result = c.get("/secret") self.assertEqual(result.status_code, 401) self.assertEqual("This is secret!", result.data.decode("utf-8")) def test_unauthorized_aborts_with_401(self): with self.app.test_client() as c: result = c.get("/secret") self.assertEqual(result.status_code, 401) def test_unauthorized_redirects_to_login_view(self): self.login_manager.login_view = "login" @self.app.route("/login") def login(): return "Login Form Goes Here!" with self.app.test_client() as c: result = c.get("/secret") self.assertEqual(result.status_code, 302) self.assertEqual(result.location, "/login?next=%2Fsecret") def test_unauthorized_with_next_in_session(self): self.login_manager.login_view = "login" self.app.config["USE_SESSION_FOR_NEXT"] = True @self.app.route("/login") def login(): return session.pop("next", "") with self.app.test_client() as c: result = c.get("/secret") self.assertEqual(result.status_code, 302) self.assertEqual(result.location, "/login") self.assertEqual(c.get("/login").data.decode("utf-8"), "/secret") def test_unauthorized_with_next_in_strong_session(self): self.login_manager.login_view = "login" self.app.config["SESSION_PROTECTION"] = "strong" self.app.config["USE_SESSION_FOR_NEXT"] = True @self.app.route("/login") def login(): if current_user.is_authenticated: # Or anything that touches current_user pass return session.pop("next", "") with self.app.test_client() as c: result = c.get("/secret") self.assertEqual(result.status_code, 302) self.assertEqual(result.location, "/login") self.assertEqual(c.get("/login").data.decode("utf-8"), "/secret") def test_unauthorized_uses_blueprint_login_view(self): with self.app.app_context(): first = Blueprint("first", "first") second = Blueprint("second", "second") @self.app.route("/app_login") def app_login(): return "Login Form Goes Here!" @self.app.route("/first_login") def first_login(): return "Login Form Goes Here!" @self.app.route("/second_login") def second_login(): return "Login Form Goes Here!" @self.app.route("/protected") @login_required def protected(): return "Access Granted" @first.route("/protected") @login_required def first_protected(): return "Access Granted" @second.route("/protected") @login_required def second_protected(): return "Access Granted" self.app.register_blueprint(first, url_prefix="/first") self.app.register_blueprint(second, url_prefix="/second") set_login_view("app_login") set_login_view("first_login", blueprint=first) set_login_view("second_login", blueprint=second) with self.app.test_client() as c: result = c.get("/protected") self.assertEqual(result.status_code, 302) expected = "/app_login?next=%2Fprotected" self.assertEqual(result.location, expected) result = c.get("/first/protected") self.assertEqual(result.status_code, 302) expected = "/first_login?next=%2Ffirst%2Fprotected" self.assertEqual(result.location, expected) result = c.get("/second/protected") self.assertEqual(result.status_code, 302) expected = "/second_login?next=%2Fsecond%2Fprotected" self.assertEqual(result.location, expected) def test_set_login_view_without_blueprints(self): with self.app.app_context(): @self.app.route("/app_login") def app_login(): return "Login Form Goes Here!" @self.app.route("/protected") @login_required def protected(): return "Access Granted" set_login_view("app_login") with self.app.test_client() as c: result = c.get("/protected") self.assertEqual(result.status_code, 302) expected = "/app_login?next=%2Fprotected" self.assertEqual(result.location, expected) # # Session Persistence/Freshness # def test_login_persists(self): with self.app.test_client() as c: c.get("/login-notch") result = c.get("/username") self.assertEqual("Notch", result.data.decode("utf-8")) def test_logout_persists(self): with self.app.test_client() as c: c.get("/login-notch") c.get("/logout") result = c.get("/username") self.assertEqual(result.data.decode("utf-8"), "Anonymous") def test_incorrect_id_logs_out(self): # Ensure that any attempt to reload the user by the ID # will seem as if the user is no longer valid @self.login_manager.user_loader def new_user_loader(user_id): return with self.app.test_client() as c: # Successfully logs in c.get("/login-notch") result = c.get("/username") self.assertEqual("Anonymous", result.data.decode("utf-8")) def test_authentication_is_fresh(self): with self.app.test_client() as c: c.get("/login-notch") fresh_result = c.get("/is-fresh") self.assertEqual("True", fresh_result.data.decode("utf-8")) remembered_result = c.get("/is-remembered") self.assertEqual("False", remembered_result.data.decode("utf-8")) def test_remember_me(self): with self.app.test_client() as c: c.get("/login-notch-remember") self._delete_session(c) username_result = c.get("/username") self.assertEqual("Notch", username_result.data.decode("utf-8")) fresh_result = c.get("/is-fresh") self.assertEqual("False", fresh_result.data.decode("utf-8")) remembered_result = c.get("/is-remembered") self.assertEqual("True", remembered_result.data.decode("utf-8")) def test_remember_me_custom_duration(self): with self.app.test_client() as c: c.get("/login-notch-remember-custom") self._delete_session(c) username_result = c.get("/username") self.assertEqual("Notch", username_result.data.decode("utf-8")) fresh_result = c.get("/is-fresh") self.assertEqual("False", fresh_result.data.decode("utf-8")) remembered_result = c.get("/is-remembered") self.assertEqual("True", remembered_result.data.decode("utf-8")) def test_remember_me_uses_custom_cookie_parameters(self): name = self.app.config["REMEMBER_COOKIE_NAME"] = "myname" duration = self.app.config["REMEMBER_COOKIE_DURATION"] = timedelta(days=2) path = self.app.config["REMEMBER_COOKIE_PATH"] = "/mypath" domain = self.app.config["REMEMBER_COOKIE_DOMAIN"] = ".localhost.local" c = self.app.test_client() c.get("/login-notch-remember") cookie = client_get_cookie(c, name, domain, path) self.assertIsNotNone(cookie) self.assertIsNotNone(cookie.expires) expected_date = datetime.now(timezone.utc) + duration difference = expected_date - cookie.expires self.assertLess(difference, timedelta(seconds=10)) self.assertGreater(difference, timedelta(seconds=-10)) def test_remember_me_custom_duration_uses_custom_cookie(self): name = self.app.config["REMEMBER_COOKIE_NAME"] = "myname" self.app.config["REMEMBER_COOKIE_DURATION"] = 172800 duration = timedelta(hours=7) path = self.app.config["REMEMBER_COOKIE_PATH"] = "/mypath" domain = self.app.config["REMEMBER_COOKIE_DOMAIN"] = ".localhost.local" c = self.app.test_client() c.get("/login-notch-remember-custom") cookie = client_get_cookie(c, name, domain, path) self.assertIsNotNone(cookie) self.assertIsNotNone(cookie.expires) expected_date = datetime.now(timezone.utc) + duration difference = expected_date - cookie.expires self.assertLess(difference, timedelta(seconds=10)) self.assertGreater(difference, timedelta(seconds=-10)) def test_remember_me_accepts_duration_as_int(self): self.app.config["REMEMBER_COOKIE_DURATION"] = 172800 duration = timedelta(seconds=172800) name = self.app.config["REMEMBER_COOKIE_NAME"] = "myname" domain = self.app.config["REMEMBER_COOKIE_DOMAIN"] = ".localhost.local" c = self.app.test_client() result = c.get("/login-notch-remember") self.assertEqual(result.status_code, 200) cookie = client_get_cookie(c, name, domain) self.assertIsNotNone(cookie) self.assertIsNotNone(cookie.expires) expected_date = datetime.now(timezone.utc) + duration difference = expected_date - cookie.expires self.assertLess(difference, timedelta(seconds=10)) self.assertGreater(difference, timedelta(seconds=-10)) def test_remember_me_with_invalid_duration_returns_500_response(self): self.app.config["REMEMBER_COOKIE_DURATION"] = "123" with self.app.test_client() as c: result = c.get("/login-notch-remember") self.assertEqual(result.status_code, 500) def test_remember_me_with_invalid_custom_duration_returns_500_resp(self): @self.app.route("/login-notch-remember-custom-invalid") def login_notch_remember_custom_invalid(): duration = "123" return str(login_user(notch, remember=True, duration=duration)) with self.app.test_client() as c: result = c.get("/login-notch-remember-custom-invalid") self.assertEqual(result.status_code, 500) def test_set_cookie_with_invalid_duration_raises_exception(self): self.app.config["REMEMBER_COOKIE_DURATION"] = "123" with self.assertRaises(Exception) as cm: with self.app.test_request_context(): session["_user_id"] = 2 self.login_manager._set_cookie(None) expected_exception_message = ( "REMEMBER_COOKIE_DURATION must be a datetime.timedelta, instead got: 123" ) self.assertIn(expected_exception_message, str(cm.exception)) def test_set_cookie_with_invalid_custom_duration_raises_exception(self): with self.assertRaises(Exception) as cm: with self.app.test_request_context(): login_user(notch, remember=True, duration="123") expected_exception_message = ( "duration must be a datetime.timedelta, instead got: 123" ) self.assertIn(expected_exception_message, str(cm.exception)) def test_remember_me_no_refresh_every_request(self): domain = self.app.config["REMEMBER_COOKIE_DOMAIN"] = ".localhost.local" path = self.app.config["REMEMBER_COOKIE_PATH"] = "/" self.app.config["REMEMBER_COOKIE_REFRESH_EACH_REQUEST"] = False c = self.app.test_client() c.get("/login-notch-remember") cookie1 = client_get_cookie(c, "remember", domain, path) self.assertIsNotNone(cookie1.expires) self._delete_session(c) c.get("/username") cookie2 = client_get_cookie(c, "remember", domain, path) self.assertEqual(cookie1.expires, cookie2.expires) def test_remember_me_refresh_each_request(self): with patch("flask_login.login_manager.datetime") as mock_dt: now = datetime.utcnow() mock_dt.utcnow = Mock(return_value=now) domain = self.app.config["REMEMBER_COOKIE_DOMAIN"] = ".localhost.local" path = self.app.config["REMEMBER_COOKIE_PATH"] = "/" self.app.config["REMEMBER_COOKIE_REFRESH_EACH_REQUEST"] = True c = self.app.test_client() c.get("/login-notch-remember") cookie1 = client_get_cookie(c, "remember", domain, path) self.assertIsNotNone(cookie1.expires) mock_dt.utcnow.return_value = now + timedelta(seconds=1) c.get("/username") cookie2 = client_get_cookie(c, "remember", domain, path) self.assertNotEqual(cookie1.expires, cookie2.expires) def test_remember_me_is_unfresh(self): with self.app.test_client() as c: c.get("/login-notch-remember") self._delete_session(c) self.assertEqual("False", c.get("/is-fresh").data.decode("utf-8")) self.assertEqual("True", c.get("/is-remembered").data.decode("utf-8")) def test_login_persists_with_signle_x_forwarded_for(self): self.app.config["SESSION_PROTECTION"] = "strong" with self.app.test_client() as c: c.get("/login-notch", headers=[("X-Forwarded-For", "10.1.1.1")]) result = c.get("/username", headers=[("X-Forwarded-For", "10.1.1.1")]) self.assertEqual("Notch", result.data.decode("utf-8")) result = c.get("/username", headers=[("X-Forwarded-For", "10.1.1.1")]) self.assertEqual("Notch", result.data.decode("utf-8")) def test_login_persists_with_many_x_forwarded_for(self): self.app.config["SESSION_PROTECTION"] = "strong" with self.app.test_client() as c: c.get("/login-notch", headers=[("X-Forwarded-For", "10.1.1.1")]) result = c.get("/username", headers=[("X-Forwarded-For", "10.1.1.1")]) self.assertEqual("Notch", result.data.decode("utf-8")) result = c.get( "/username", headers=[("X-Forwarded-For", "10.1.1.1, 10.1.1.2")] ) self.assertEqual("Notch", result.data.decode("utf-8")) def test_user_loaded_from_cookie_fired(self): with self.app.test_client() as c: c.get("/login-notch-remember") self._delete_session(c) with listen_to(user_loaded_from_cookie) as listener: c.get("/username") listener.assert_heard_one(self.app, user=notch) def test_user_loaded_from_request_fired(self): user_id = 1 user_name = USERS[user_id].name with self.app.test_client() as c: with listen_to(user_loaded_from_request) as listener: url = f"/username?user_id={user_id}" result = c.get(url) self.assertEqual(user_name, result.data.decode("utf-8")) listener.assert_heard_one(self.app, user=USERS[user_id]) def test_logout_stays_logged_out_with_remember_me(self): with self.app.test_client() as c: c.get("/login-notch-remember") c.get("/logout") result = c.get("/username") self.assertEqual(result.data.decode("utf-8"), "Anonymous") def test_logout_stays_logged_out_with_remember_me_custom_duration(self): with self.app.test_client() as c: c.get("/login-notch-remember-custom") c.get("/logout") result = c.get("/username") self.assertEqual(result.data.decode("utf-8"), "Anonymous") def test_needs_refresh_uses_handler(self): @self.login_manager.needs_refresh_handler def _on_refresh(): return "Needs Refresh!" with self.app.test_client() as c: c.get("/login-notch-remember") result = c.get("/needs-refresh") self.assertEqual("Needs Refresh!", result.data.decode("utf-8")) def test_needs_refresh_fires_needs_refresh_signal(self): with self.app.test_client() as c: c.get("/login-notch-remember") with listen_to(user_needs_refresh) as listener: c.get("/needs-refresh") listener.assert_heard_one(self.app) def test_needs_refresh_fires_flash_when_redirect_to_refresh_view(self): self.login_manager.refresh_view = "/refresh_view" self.login_manager.needs_refresh_message = "Refresh" self.login_manager.needs_refresh_message_category = "refresh" category_filter = [self.login_manager.needs_refresh_message_category] with self.app.test_client() as c: c.get("/login-notch-remember") c.get("/needs-refresh") msgs = get_flashed_messages(category_filter=category_filter) self.assertIn(self.login_manager.needs_refresh_message, msgs) def test_needs_refresh_flash_message_localized(self): def _gettext(msg): if msg == "Refresh": return "Aktualisieren" self.login_manager.refresh_view = "/refresh_view" self.login_manager.localize_callback = _gettext self.login_manager.needs_refresh_message = "Refresh" self.login_manager.needs_refresh_message_category = "refresh" category_filter = [self.login_manager.needs_refresh_message_category] with self.app.test_client() as c: c.get("/login-notch-remember") c.get("/needs-refresh") msgs = get_flashed_messages(category_filter=category_filter) self.assertIn("Aktualisieren", msgs) self.login_manager.localize_callback = None def test_needs_refresh_aborts_401(self): with self.app.test_client() as c: c.get("/login-notch-remember") result = c.get("/needs-refresh") self.assertEqual(result.status_code, 401) def test_redirects_to_refresh_view(self): @self.app.route("/refresh-view") def refresh_view(): return "" self.login_manager.refresh_view = "refresh_view" with self.app.test_client() as c: c.get("/login-notch-remember") result = c.get("/needs-refresh") self.assertEqual(result.status_code, 302) expected = "/refresh-view?next=%2Fneeds-refresh" self.assertEqual(result.location, expected) def test_refresh_with_next_in_session(self): @self.app.route("/refresh-view") def refresh_view(): return session.pop("next", "") self.login_manager.refresh_view = "refresh_view" self.app.config["USE_SESSION_FOR_NEXT"] = True with self.app.test_client() as c: c.get("/login-notch-remember") result = c.get("/needs-refresh") self.assertEqual(result.status_code, 302) self.assertEqual(result.location, "/refresh-view") result = c.get("/refresh-view") self.assertEqual(result.data.decode("utf-8"), "/needs-refresh") def test_confirm_login(self): with self.app.test_client() as c: c.get("/login-notch-remember") self._delete_session(c) self.assertEqual("False", c.get("/is-fresh").data.decode("utf-8")) self.assertEqual("True", c.get("/is-remembered").data.decode("utf-8")) c.get("/confirm-login") self.assertEqual("True", c.get("/is-fresh").data.decode("utf-8")) self.assertEqual("True", c.get("/is-remembered").data.decode("utf-8")) def test_user_login_confirmed_signal_fired(self): with self.app.test_client() as c: with listen_to(user_login_confirmed) as listener: c.get("/confirm-login") listener.assert_heard_one(self.app) def test_session_not_modified(self): with self.app.test_client() as c: # Within the request we think we didn't modify the session. self.assertEqual( "modified=False", c.get("/empty_session").data.decode("utf-8") ) # But after the request, the session could be modified by the # "after_request" handlers that call _update_remember_cookie. # Ensure that if nothing changed the session is not modified. self.assertFalse(session.modified) def test_invalid_remember_cookie(self): domain = self.app.config["REMEMBER_COOKIE_DOMAIN"] = ".localhost.local" c = self.app.test_client() c.get("/login-notch-remember") with c.session_transaction() as sess: sess["_user_id"] = None client_set_cookie(c, self.remember_cookie_name, "foo", domain=domain) result = c.get("/username") self.assertEqual("Anonymous", result.data.decode("utf-8")) # # Session Protection # def test_session_protection_basic_passes_successive_requests(self): self.app.config["SESSION_PROTECTION"] = "basic" with self.app.test_client() as c: c.get("/login-notch-remember") username_result = c.get("/username") self.assertEqual("Notch", username_result.data.decode("utf-8")) fresh_result = c.get("/is-fresh") self.assertEqual("True", fresh_result.data.decode("utf-8")) def test_session_protection_strong_passes_successive_requests(self): self.app.config["SESSION_PROTECTION"] = "strong" with self.app.test_client() as c: c.get("/login-notch-remember") username_result = c.get("/username") self.assertEqual("Notch", username_result.data.decode("utf-8")) fresh_result = c.get("/is-fresh") self.assertEqual("True", fresh_result.data.decode("utf-8")) def test_session_protection_basic_marks_session_unfresh(self): self.app.config["SESSION_PROTECTION"] = "basic" with self.app.test_client() as c: c.get("/login-notch-remember") username_result = c.get("/username", headers=[("User-Agent", "different")]) self.assertEqual("Notch", username_result.data.decode("utf-8")) fresh_result = c.get("/is-fresh") self.assertEqual("False", fresh_result.data.decode("utf-8")) def test_session_protection_basic_fires_signal(self): self.app.config["SESSION_PROTECTION"] = "basic" with self.app.test_client() as c: c.get("/login-notch-remember") with listen_to(session_protected) as listener: c.get("/username", headers=[("User-Agent", "different")]) listener.assert_heard_one(self.app) def test_session_protection_basic_skips_when_remember_me(self): self.app.config["SESSION_PROTECTION"] = "basic" with self.app.test_client() as c: c.get("/login-notch-remember") # clear session to force remember me (and remove old session id) self._delete_session(c) # should not trigger protection because "sess" is empty with listen_to(session_protected) as listener: c.get("/username") listener.assert_heard_none(self.app) def test_session_protection_strong_skips_when_remember_me(self): self.app.config["SESSION_PROTECTION"] = "strong" with self.app.test_client() as c: c.get("/login-notch-remember") # clear session to force remember me (and remove old session id) self._delete_session(c) # should not trigger protection because "sess" is empty with listen_to(session_protected) as listener: c.get("/username") listener.assert_heard_none(self.app) def test_permanent_strong_session_protection_marks_session_unfresh(self): self.app.config["SESSION_PROTECTION"] = "strong" with self.app.test_client() as c: c.get("/login-notch-permanent") username_result = c.get("/username", headers=[("User-Agent", "different")]) self.assertEqual("Notch", username_result.data.decode("utf-8")) fresh_result = c.get("/is-fresh") self.assertEqual("False", fresh_result.data.decode("utf-8")) def test_permanent_strong_session_protection_fires_signal(self): self.app.config["SESSION_PROTECTION"] = "strong" with self.app.test_client() as c: c.get("/login-notch-permanent") with listen_to(session_protected) as listener: c.get("/username", headers=[("User-Agent", "different")]) listener.assert_heard_one(self.app) def test_session_protection_strong_deletes_session(self): self.app.config["SESSION_PROTECTION"] = "strong" with self.app.test_client() as c: # write some unrelated data in the session, to ensure it does not # get destroyed with c.session_transaction() as sess: sess["foo"] = "bar" c.get("/login-notch-remember") username_result = c.get("/username", headers=[("User-Agent", "different")]) self.assertEqual("Anonymous", username_result.data.decode("utf-8")) with c.session_transaction() as sess: self.assertIn("foo", sess) self.assertEqual("bar", sess["foo"]) def test_session_protection_strong_fires_signal_user_agent(self): self.app.config["SESSION_PROTECTION"] = "strong" with self.app.test_client() as c: c.get("/login-notch-remember") with listen_to(session_protected) as listener: c.get("/username", headers=[("User-Agent", "different")]) listener.assert_heard_one(self.app) def test_session_protection_strong_fires_signal_x_forwarded_for(self): self.app.config["SESSION_PROTECTION"] = "strong" with self.app.test_client() as c: c.get("/login-notch-remember", headers=[("X-Forwarded-For", "10.1.1.1")]) with listen_to(session_protected) as listener: c.get("/username", headers=[("X-Forwarded-For", "10.1.1.2")]) listener.assert_heard_one(self.app) def test_session_protection_skip_when_off_and_anonymous(self): with self.app.test_client() as c: # no user access with listen_to(user_accessed) as user_listener: results = c.get("/") user_listener.assert_heard_none(self.app) # access user with no session data with listen_to(session_protected) as session_listener: results = c.get("/username") self.assertEqual(results.data.decode("utf-8"), "Anonymous") session_listener.assert_heard_none(self.app) # verify no session data has been set self.assertFalse(session) def test_session_protection_skip_when_basic_and_anonymous(self): self.app.config["SESSION_PROTECTION"] = "basic" with self.app.test_client() as c: # no user access with listen_to(user_accessed) as user_listener: results = c.get("/") user_listener.assert_heard_none(self.app) # access user with no session data with listen_to(session_protected) as session_listener: results = c.get("/username") self.assertEqual(results.data.decode("utf-8"), "Anonymous") session_listener.assert_heard_none(self.app) # verify no session data has been set self.assertFalse(session) # # Lazy Access User # def test_requests_without_accessing_session(self): with self.app.test_client() as c: c.get("/login-notch") # no session access with listen_to(user_accessed) as listener: c.get("/") listener.assert_heard_none(self.app) # should have a session access with listen_to(user_accessed) as listener: result = c.get("/username") listener.assert_heard_one(self.app) self.assertEqual(result.data.decode("utf-8"), "Notch") # # View Decorators # def test_login_required_decorator(self): @self.app.route("/protected") @login_required def protected(): return "Access Granted" with self.app.test_client() as c: result = c.get("/protected") self.assertEqual(result.status_code, 401) c.get("/login-notch") result2 = c.get("/protected") self.assertIn("Access Granted", result2.data.decode("utf-8")) @unittest.skipIf(not hasattr(Flask, "ensure_sync"), "Flask version before async") def test_login_required_decorator_with_async(self): import asyncio @self.app.route("/protected") @login_required async def protected(): await asyncio.sleep(0) return "Access Granted" with self.app.test_client() as c: self.app.config["LOGIN_DISABLED"] = True result = c.get("/protected") self.assertEqual(result.status_code, 200) self.app.config["LOGIN_DISABLED"] = False result = c.get("/protected") self.assertEqual(result.status_code, 401) c.get("/login-notch") result = c.get("/protected") self.assertEqual(result.status_code, 200) c.get("/login-notch") result2 = c.get("/protected") self.assertIn("Access Granted", result2.data.decode("utf-8")) def test_decorators_are_disabled(self): @self.app.route("/protected") @login_required @fresh_login_required def protected(): return "Access Granted" self.app.config["LOGIN_DISABLED"] = True with self.app.test_client() as c: result = c.get("/protected") self.assertIn("Access Granted", result.data.decode("utf-8")) def test_fresh_login_required_decorator(self): @self.app.route("/very-protected") @fresh_login_required def very_protected(): return "Access Granted" with self.app.test_client() as c: result = c.get("/very-protected") self.assertEqual(result.status_code, 401) c.get("/login-notch-remember") logged_in_result = c.get("/very-protected") self.assertEqual("Access Granted", logged_in_result.data.decode("utf-8")) self._delete_session(c) stale_result = c.get("/very-protected") self.assertEqual(stale_result.status_code, 401) c.get("/confirm-login") refreshed_result = c.get("/very-protected") self.assertEqual("Access Granted", refreshed_result.data.decode("utf-8")) # # Misc # def test_user_context_processor(self): with self.app.test_request_context(): _ucp = self.app.context_processor(_user_context_processor) self.assertIsInstance(_ucp()["current_user"], AnonymousUserMixin) class LoginViaRequestTestCase(unittest.TestCase): """Tests for LoginManager.request_loader.""" def setUp(self): self.app = Flask(__name__) self.app.config["SECRET_KEY"] = "deterministic" self.app.config["SESSION_PROTECTION"] = None self.remember_cookie_name = "remember" self.app.config["REMEMBER_COOKIE_NAME"] = self.remember_cookie_name self.login_manager = LoginManager() self.login_manager.init_app(self.app) self.app.config["LOGIN_DISABLED"] = False @self.app.route("/") def index(): return "Welcome!" @self.app.route("/login-notch") def login_notch(): return str(login_user(notch)) @self.app.route("/username") def username(): if current_user.is_authenticated: return current_user.name return "Anonymous", 401 @self.app.route("/logout") def logout(): return str(logout_user()) @self.login_manager.request_loader def load_user_from_request(request): user_id = request.args.get("user_id") or session.get("_user_id") try: user_id = int(float(user_id)) except TypeError: pass return USERS.get(user_id) # This will help us with the possibility of typoes in the tests. Now # we shouldn't have to check each response to help us set up state # (such as login pages) to make sure it worked: we will always # get an exception raised (rather than return a 404 response) @self.app.errorhandler(404) def handle_404(e): raise e unittest.TestCase.setUp(self) def test_has_no_user_loader_callback(self): self.assertIsNone(self.login_manager._user_callback) def test_request_context_users_are_anonymous(self): with self.app.test_request_context(): self.assertTrue(current_user.is_anonymous) def test_defaults_anonymous(self): with self.app.test_client() as c: result = c.get("/username") self.assertEqual(result.status_code, 401) def test_login_via_request(self): user_id = 2 user_name = USERS[user_id].name with self.app.test_client() as c: url = f"/username?user_id={user_id}" result = c.get(url) self.assertEqual(user_name, result.data.decode("utf-8")) def test_login_via_request_uses_cookie_when_already_logged_in(self): user_id = 2 user_name = notch.name with self.app.test_client() as c: c.get("/login-notch") url = "/username" result = c.get(url) self.assertEqual(user_name, result.data.decode("utf-8")) url = f"/username?user_id={user_id}" result = c.get(url) self.assertEqual("Steve", result.data.decode("utf-8")) def test_login_invalid_user_with_request(self): user_id = 9000 with self.app.test_client() as c: url = f"/username?user_id={user_id}" result = c.get(url) self.assertEqual(result.status_code, 401) def test_login_invalid_user_with_request_when_already_logged_in(self): user_id = 9000 with self.app.test_client() as c: url = "/login-notch" result = c.get(url) self.assertEqual("True", result.data.decode("utf-8")) url = f"/username?user_id={user_id}" result = c.get(url) self.assertEqual(result.status_code, 401) def test_login_user_with_request_does_not_modify_session(self): user_id = 2 user_name = USERS[user_id].name with self.app.test_client() as c: url = f"/username?user_id={user_id}" result = c.get(url) self.assertEqual(user_name, result.data.decode("utf-8")) url = "/username" result = c.get(url) self.assertEqual("Anonymous", result.data.decode("utf-8")) class TestLoginUrlGeneration(unittest.TestCase): def setUp(self): self.app = Flask(__name__) self.login_manager = LoginManager() self.login_manager.init_app(self.app) @self.app.route("/login") def login(): return "" def test_make_next_param(self): with self.app.test_request_context(): url = make_next_param("/login", "http://localhost/profile") self.assertEqual("/profile", url) url = make_next_param("https://localhost/login", "http://localhost/profile") self.assertEqual("http://localhost/profile", url) url = make_next_param( "http://accounts.localhost/login", "http://localhost/profile" ) self.assertEqual("http://localhost/profile", url) def test_login_url_generation(self): with self.app.test_request_context(): PROTECTED = "http://localhost/protected" self.assertEqual( "/login?n=%2Fprotected", login_url("/login", PROTECTED, "n") ) url = login_url("/login", PROTECTED) self.assertEqual("/login?next=%2Fprotected", url) expected = ( "https://auth.localhost/login?next=http%3A%2F%2Flocalhost%2Fprotected" ) result = login_url("https://auth.localhost/login", PROTECTED) self.assertEqual(expected, result) self.assertEqual( "/login?affil=cgnu&next=%2Fprotected", login_url("/login?affil=cgnu", PROTECTED), ) def test_login_url_generation_with_view(self): with self.app.test_request_context(): self.assertEqual( "/login?next=%2Fprotected", login_url("login", "/protected") ) def test_login_url_no_next_url(self): self.assertEqual(login_url("/foo"), "/foo") class CookieEncodingTestCase(unittest.TestCase): def test_cookie_encoding(self): app = Flask(__name__) app.config["SECRET_KEY"] = "deterministic" # COOKIE = u'1|7d276051c1eec578ed86f6b8478f7f7d803a7970' # Due to the restriction of 80 chars I have to break up the hash in two h1 = "0e9e6e9855fbe6df7906ec4737578a1d491b38d3fd5246c1561016e189d6516" h2 = "043286501ca43257c938e60aad77acec5ce916b94ca9d00c0bb6f9883ae4b82" h3 = "ae" COOKIE = "1|" + h1 + h2 + h3 with app.test_request_context(): self.assertEqual(COOKIE, encode_cookie("1")) self.assertEqual("1", decode_cookie(COOKIE)) self.assertIsNone(decode_cookie("Foo|BAD_BASH")) self.assertIsNone(decode_cookie("no bar")) def test_cookie_encoding_with_key(self): app = Flask(__name__) app.config["SECRET_KEY"] = "not-used" key = "deterministic" # COOKIE = u'1|7d276051c1eec578ed86f6b8478f7f7d803a7970' # Due to the restriction of 80 chars I have to break up the hash in two h1 = "0e9e6e9855fbe6df7906ec4737578a1d491b38d3fd5246c1561016e189d6516" h2 = "043286501ca43257c938e60aad77acec5ce916b94ca9d00c0bb6f9883ae4b82" h3 = "ae" COOKIE = "1|" + h1 + h2 + h3 with app.test_request_context(): self.assertEqual(COOKIE, encode_cookie("1", key=key)) self.assertEqual("1", decode_cookie(COOKIE, key=key)) self.assertIsNone(decode_cookie("Foo|BAD_BASH", key=key)) self.assertIsNone(decode_cookie("no bar", key=key)) class SecretKeyTestCase(unittest.TestCase): def setUp(self): self.app = Flask(__name__) def test_bytes(self): self.app.config["SECRET_KEY"] = b"\x9e\x8f\x14" with self.app.test_request_context(): self.assertEqual(_secret_key(), b"\x9e\x8f\x14") def test_native(self): self.app.config["SECRET_KEY"] = "\x9e\x8f\x14" with self.app.test_request_context(): self.assertEqual(_secret_key(), b"\x9e\x8f\x14") def test_default(self): self.assertEqual(_secret_key("\x9e\x8f\x14"), b"\x9e\x8f\x14") class ImplicitIdUser(UserMixin): __slots__ = () def __init__(self, id): self.id = id class ExplicitIdUser(UserMixin): __slots__ = () def __init__(self, name): self.name = name class UserMixinTestCase(unittest.TestCase): def test_default_values(self): user = ImplicitIdUser(1) self.assertTrue(user.is_active) self.assertTrue(user.is_authenticated) self.assertFalse(user.is_anonymous) def test_get_id_from_id_attribute(self): user = ImplicitIdUser(1) self.assertEqual("1", user.get_id()) def test_get_id_not_implemented(self): user = ExplicitIdUser("Notch") self.assertRaises(NotImplementedError, lambda: user.get_id()) def test_equality(self): first = ImplicitIdUser(1) same = ImplicitIdUser(1) different = ImplicitIdUser(2) # Explicitly test the equality operator self.assertTrue(first == same) self.assertFalse(first == different) self.assertFalse(first != same) self.assertTrue(first != different) self.assertFalse(first == "1") self.assertTrue(first != "1") def test_hashable(self): self.assertTrue(isinstance(UserMixin(), Hashable)) class AnonymousUserTestCase(unittest.TestCase): def test_values(self): user = AnonymousUserMixin() self.assertFalse(user.is_active) self.assertFalse(user.is_authenticated) self.assertTrue(user.is_anonymous) self.assertIsNone(user.get_id()) class UnicodeCookieUserIDTestCase(unittest.TestCase): def setUp(self): self.app = Flask(__name__) self.app.config["SECRET_KEY"] = "deterministic" self.app.config["SESSION_PROTECTION"] = None self.remember_cookie_name = "remember" self.app.config["REMEMBER_COOKIE_NAME"] = self.remember_cookie_name self.login_manager = LoginManager() self.login_manager.init_app(self.app) self.app.config["LOGIN_DISABLED"] = False @self.app.route("/") def index(): return "Welcome!" @self.app.route("/login-germanjapanese-remember") def login_germanjapanese_remember(): return str(login_user(germanjapanese, remember=True)) @self.app.route("/username") def username(): if current_user.is_authenticated: return current_user.name return "Anonymous" @self.app.route("/userid") def user_id(): if current_user.is_authenticated: return current_user.id return "wrong_id" @self.login_manager.user_loader def load_user(user_id): return USERS[str(user_id)] # This will help us with the possibility of typoes in the tests. Now # we shouldn't have to check each response to help us set up state # (such as login pages) to make sure it worked: we will always # get an exception raised (rather than return a 404 response) @self.app.errorhandler(404) def handle_404(e): raise e unittest.TestCase.setUp(self) def _delete_session(self, c): # Helper method to cause the session to be deleted # as if the browser was closed. This will remove # the session regardless of the permanent flag # on the session! with c.session_transaction() as sess: sess.clear() def test_remember_me_username(self): with self.app.test_client() as c: c.get("/login-germanjapanese-remember") self._delete_session(c) result = c.get("/username") self.assertEqual("Müller", result.data.decode("utf-8")) def test_remember_me_user_id(self): with self.app.test_client() as c: c.get("/login-germanjapanese-remember") self._delete_session(c) result = c.get("/userid") self.assertEqual("佐藤", result.data.decode("utf-8")) class StrictHostForRedirectsTestCase(unittest.TestCase): def setUp(self): self.app = Flask(__name__) self.app.config["SECRET_KEY"] = "deterministic" self.app.config["SESSION_PROTECTION"] = None self.remember_cookie_name = "remember" self.app.config["REMEMBER_COOKIE_NAME"] = self.remember_cookie_name self.login_manager = LoginManager() self.login_manager.init_app(self.app) self.app.config["LOGIN_DISABLED"] = False @self.app.route("/secret") def secret(): return self.login_manager.unauthorized() @self.app.route("/") def index(): return "Welcome!" @self.login_manager.user_loader def load_user(user_id): return USERS[str(user_id)] # This will help us with the possibility of typoes in the tests. Now # we shouldn't have to check each response to help us set up state # (such as login pages) to make sure it worked: we will always # get an exception raised (rather than return a 404 response) @self.app.errorhandler(404) def handle_404(e): raise e unittest.TestCase.setUp(self) def test_unauthorized_uses_host_from_next_url(self): self.login_manager.login_view = "login" self.app.config["FORCE_HOST_FOR_REDIRECTS"] = None @self.app.route("/login") def login(): return session.pop("next", "") with self.app.test_client() as c: result = c.get("/secret", base_url="http://foo.com") self.assertEqual(result.status_code, 302) self.assertEqual(result.location, "/login?next=%2Fsecret") def test_unauthorized_uses_host_from_config_when_available(self): self.login_manager.login_view = "login" self.app.config["FORCE_HOST_FOR_REDIRECTS"] = "good.com" @self.app.route("/login") def login(): return session.pop("next", "") with self.app.test_client() as c: result = c.get("/secret", base_url="http://bad.com") self.assertEqual(result.status_code, 302) self.assertEqual(result.location, "//good.com/login?next=%2Fsecret") def test_unauthorized_uses_host_from_x_forwarded_for_header(self): self.login_manager.login_view = "login" self.app.config["FORCE_HOST_FOR_REDIRECTS"] = None self.app.wsgi_app = ProxyFix(self.app.wsgi_app, x_host=1) @self.app.route("/login") def login(): return session.pop("next", "") with self.app.test_client() as c: headers = { "X-Forwarded-Host": "proxy.com", } result = c.get("/secret", base_url="http://foo.com", headers=headers) self.assertEqual(result.status_code, 302) self.assertEqual(result.location, "/login?next=%2Fsecret") def test_unauthorized_ignores_host_from_x_forwarded_for_header(self): self.login_manager.login_view = "login" self.app.config["FORCE_HOST_FOR_REDIRECTS"] = "good.com" @self.app.route("/login") def login(): return session.pop("next", "") with self.app.test_client() as c: headers = { "X-Forwarded-Host": "proxy.com", } result = c.get("/secret", base_url="http://foo.com", headers=headers) self.assertEqual(result.status_code, 302) assert result.location == "//good.com/login?next=%2Fsecret" class CustomTestClientTestCase(unittest.TestCase): def setUp(self): self.app = Flask(__name__) self.app.config["SECRET_KEY"] = "deterministic" self.app.config["SESSION_PROTECTION"] = None self.remember_cookie_name = "remember" self.app.config["REMEMBER_COOKIE_NAME"] = self.remember_cookie_name self.login_manager = LoginManager() self.login_manager.init_app(self.app) self.app.config["LOGIN_DISABLED"] = False self.app.test_client_class = FlaskLoginClient @self.app.route("/username") def username(): if current_user.is_authenticated: return current_user.name return "Anonymous" @self.app.route("/is-fresh") def is_fresh(): return str(login_fresh()) @self.login_manager.user_loader def load_user(user_id): return USERS[int(user_id)] # This will help us with the possibility of typoes in the tests. Now # we shouldn't have to check each response to help us set up state # (such as login pages) to make sure it worked: we will always # get an exception raised (rather than return a 404 response) @self.app.errorhandler(404) def handle_404(e): raise e unittest.TestCase.setUp(self) def test_no_args_to_test_client(self): with self.app.test_client() as c: result = c.get("/username") self.assertEqual("Anonymous", result.data.decode("utf-8")) def test_user_arg_to_test_client(self): with self.app.test_client(user=notch) as c: username = c.get("/username") self.assertEqual("Notch", username.data.decode("utf-8")) is_fresh = c.get("/is-fresh") self.assertEqual("True", is_fresh.data.decode("utf-8")) def test_fresh_login_arg_to_test_client(self): with self.app.test_client(user=notch, fresh_login=False) as c: username = c.get("/username") self.assertEqual("Notch", username.data.decode("utf-8")) is_fresh = c.get("/is-fresh") self.assertEqual("False", is_fresh.data.decode("utf-8")) def test_session_protection_modes(self): # Disabled self.app.config["SESSION_PROTECTION"] = None with self.app.test_client(user=notch, fresh_login=False) as c: username = c.get("/username") self.assertEqual("Notch", username.data.decode("utf-8")) is_fresh = c.get("/is-fresh") self.assertEqual("False", is_fresh.data.decode("utf-8")) with self.app.test_client(user=notch, fresh_login=True) as c: username = c.get("/username") self.assertEqual("Notch", username.data.decode("utf-8")) is_fresh = c.get("/is-fresh") self.assertEqual("True", is_fresh.data.decode("utf-8")) # Enabled with mode: basic self.app.config["SESSION_PROTECTION"] = "basic" with self.app.test_client(user=notch, fresh_login=False) as c: username = c.get("/username") self.assertEqual("Notch", username.data.decode("utf-8")) is_fresh = c.get("/is-fresh") self.assertEqual("False", is_fresh.data.decode("utf-8")) with self.app.test_client(user=notch, fresh_login=True) as c: username = c.get("/username") self.assertEqual("Notch", username.data.decode("utf-8")) is_fresh = c.get("/is-fresh") self.assertEqual("False", is_fresh.data.decode("utf-8")) # Enabled with mode: strong self.app.config["SESSION_PROTECTION"] = "strong" with self.app.test_client(user=notch, fresh_login=False) as c: username = c.get("/username") self.assertEqual("Anonymous", username.data.decode("utf-8")) is_fresh = c.get("/is-fresh") self.assertEqual("False", is_fresh.data.decode("utf-8")) with self.app.test_client(user=notch, fresh_login=True) as c: username = c.get("/username") self.assertEqual("Anonymous", username.data.decode("utf-8")) is_fresh = c.get("/is-fresh") self.assertEqual("False", is_fresh.data.decode("utf-8")) flask-login-0.6.3/tox.ini000066400000000000000000000010721451774104400152500ustar00rootroot00000000000000[tox] envlist = py3{11,10,9,8,7},pypy3{8,7} py39-min style skip_missing_interpreters = true [testenv] deps = -r requirements/tests.txt min: -r requirements/tests-min.txt commands = coverage run --source=flask_login --module \ pytest -v --tb=short --basetemp={envtmpdir} {posargs} coverage report [testenv:style] deps = -r requirements/style.txt skip_install = true commands = pre-commit run --all-files [testenv:docs] deps = -r requirements/docs.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html