pax_global_header 0000666 0000000 0000000 00000000064 14517741044 0014521 g ustar 00root root 0000000 0000000 52 comment=88eca62d4f87a36b1339ea8fe5a8b7ad878cf3ef
flask-login-0.6.3/ 0000775 0000000 0000000 00000000000 14517741044 0013735 5 ustar 00root root 0000000 0000000 flask-login-0.6.3/.editorconfig 0000664 0000000 0000000 00000000331 14517741044 0016407 0 ustar 00root root 0000000 0000000 root = 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-revs 0000664 0000000 0000000 00000000406 14517741044 0020035 0 ustar 00root root 0000000 0000000 # move to src directory
610b99845e667c81e2d422531476f2ea4fecdb04
# apply pyupgrade
6032c9a193170f11ef9c6e0d225c41e9c12d3d4a
# apply reorder_python_imports
379166376da287bf4ee5b2e87e25c8ebf1b2bfd1
# apply black formatting
379166376da287bf4ee5b2e87e25c8ebf1b2bfd1
flask-login-0.6.3/.github/ 0000775 0000000 0000000 00000000000 14517741044 0015275 5 ustar 00root root 0000000 0000000 flask-login-0.6.3/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 14517741044 0017460 5 ustar 00root root 0000000 0000000 flask-login-0.6.3/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000001502 14517741044 0022150 0 ustar 00root root 0000000 0000000 ---
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.yml 0000664 0000000 0000000 00000000247 14517741044 0020130 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
day: "monday"
time: "16:00"
timezone: "UTC"
flask-login-0.6.3/.github/workflows/ 0000775 0000000 0000000 00000000000 14517741044 0017332 5 ustar 00root root 0000000 0000000 flask-login-0.6.3/.github/workflows/lock.yaml 0000664 0000000 0000000 00000000521 14517741044 0021144 0 ustar 00root root 0000000 0000000 name: '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.yaml 0000664 0000000 0000000 00000000632 14517741044 0022273 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000001336 14517741044 0023144 0 ustar 00root root 0000000 0000000 on:
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.yaml 0000664 0000000 0000000 00000002230 14517741044 0021355 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000000202 14517741044 0015717 0 ustar 00root root 0000000 0000000 .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.yaml 0000664 0000000 0000000 00000001630 14517741044 0020216 0 ustar 00root root 0000000 0000000 ci:
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.yaml 0000664 0000000 0000000 00000000321 14517741044 0017160 0 ustar 00root root 0000000 0000000 version: 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.md 0000664 0000000 0000000 00000015532 14517741044 0015335 0 ustar 00root root 0000000 0000000 Flask-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.md 0000664 0000000 0000000 00000003134 14517741044 0016167 0 ustar 00root root 0000000 0000000 # 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/LICENSE 0000664 0000000 0000000 00000002043 14517741044 0014741 0 ustar 00root root 0000000 0000000 Copyright (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.in 0000664 0000000 0000000 00000000236 14517741044 0015474 0 ustar 00root root 0000000 0000000 include 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.md 0000664 0000000 0000000 00000011035 14517741044 0015214 0 ustar 00root root 0000000 0000000 # Flask-Login

[](https://coveralls.io/github/maxcountryman/flask-login?branch=main)
[](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/ 0000775 0000000 0000000 00000000000 14517741044 0014665 5 ustar 00root root 0000000 0000000 flask-login-0.6.3/docs/Makefile 0000664 0000000 0000000 00000001176 14517741044 0016332 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 14517741044 0016311 5 ustar 00root root 0000000 0000000 flask-login-0.6.3/docs/_themes/LICENSE 0000664 0000000 0000000 00000003375 14517741044 0017326 0 ustar 00root root 0000000 0000000 Copyright (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/README 0000664 0000000 0000000 00000002105 14517741044 0017167 0 ustar 00root root 0000000 0000000 Flask 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/ 0000775 0000000 0000000 00000000000 14517741044 0017411 5 ustar 00root root 0000000 0000000 flask-login-0.6.3/docs/_themes/flask/layout.html 0000664 0000000 0000000 00000001106 14517741044 0021612 0 ustar 00root root 0000000 0000000 {%- 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.html 0000664 0000000 0000000 00000001116 14517741044 0022276 0 ustar 00root root 0000000 0000000
{% endif %}
{% endblock %}
{# do not display relbars #}
{% block relbar1 %}{% endblock %}
{% block relbar2 %}
{% if theme_github_fork %}
{% endif %}
{% endblock %}
{% block sidebar1 %}{% endblock %}
{% block sidebar2 %}{% endblock %}
flask-login-0.6.3/docs/_themes/flask_small/static/ 0000775 0000000 0000000 00000000000 14517741044 0022070 5 ustar 00root root 0000000 0000000 flask-login-0.6.3/docs/_themes/flask_small/static/flasky.css_t 0000664 0000000 0000000 00000010753 14517741044 0024424 0 ustar 00root root 0000000 0000000 /*
* 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.conf 0000664 0000000 0000000 00000000270 14517741044 0022551 0 ustar 00root root 0000000 0000000 [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.py 0000664 0000000 0000000 00000010052 14517741044 0022737 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000017005 14517741044 0016167 0 ustar 00root root 0000000 0000000 #
# 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.rst 0000664 0000000 0000000 00000060670 14517741044 0016537 0 ustar 00root root 0000000 0000000 ===========
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.bat 0000664 0000000 0000000 00000001401 14517741044 0016266 0 ustar 00root root 0000000 0000000 @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/ 0000775 0000000 0000000 00000000000 14517741044 0016460 5 ustar 00root root 0000000 0000000 flask-login-0.6.3/requirements/ci-release.in 0000664 0000000 0000000 00000000034 14517741044 0021016 0 ustar 00root root 0000000 0000000 build
twine
readme-renderer
flask-login-0.6.3/requirements/ci-release.txt 0000664 0000000 0000000 00000002305 14517741044 0021232 0 ustar 00root root 0000000 0000000 # 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.in 0000664 0000000 0000000 00000000016 14517741044 0020540 0 ustar 00root root 0000000 0000000 tox
coveralls
flask-login-0.6.3/requirements/ci-tests.txt 0000664 0000000 0000000 00000001613 14517741044 0020755 0 ustar 00root root 0000000 0000000 # 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.in 0000664 0000000 0000000 00000000071 14517741044 0017564 0 ustar 00root root 0000000 0000000 -r docs.in
-r style.in
-r tests.in
pip-compile-multi
tox
flask-login-0.6.3/requirements/dev.txt 0000664 0000000 0000000 00000001402 14517741044 0017774 0 ustar 00root root 0000000 0000000 # 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.in 0000664 0000000 0000000 00000000007 14517741044 0017735 0 ustar 00root root 0000000 0000000 Sphinx
flask-login-0.6.3/requirements/docs.txt 0000664 0000000 0000000 00000002155 14517741044 0020154 0 ustar 00root root 0000000 0000000 # 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.in 0000664 0000000 0000000 00000000013 14517741044 0020142 0 ustar 00root root 0000000 0000000 pre-commit
flask-login-0.6.3/requirements/style.txt 0000664 0000000 0000000 00000001114 14517741044 0020356 0 ustar 00root root 0000000 0000000 # 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.in 0000664 0000000 0000000 00000000137 14517741044 0020734 0 ustar 00root root 0000000 0000000 flask==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.txt 0000664 0000000 0000000 00000001111 14517741044 0021136 0 ustar 00root root 0000000 0000000 # 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.in 0000664 0000000 0000000 00000000061 14517741044 0020147 0 ustar 00root root 0000000 0000000 blinker
coverage
asgiref
pytest
semantic_version
flask-login-0.6.3/requirements/tests.txt 0000664 0000000 0000000 00000000767 14517741044 0020375 0 ustar 00root root 0000000 0000000 # 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.cfg 0000664 0000000 0000000 00000003614 14517741044 0015562 0 ustar 00root root 0000000 0000000 [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.py 0000664 0000000 0000000 00000000334 14517741044 0015447 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14517741044 0014524 5 ustar 00root root 0000000 0000000 flask-login-0.6.3/src/flask_login/ 0000775 0000000 0000000 00000000000 14517741044 0017014 5 ustar 00root root 0000000 0000000 flask-login-0.6.3/src/flask_login/__about__.py 0000664 0000000 0000000 00000000605 14517741044 0021275 0 ustar 00root root 0000000 0000000 __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__.py 0000664 0000000 0000000 00000005171 14517741044 0021131 0 ustar 00root root 0000000 0000000 from .__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.py 0000664 0000000 0000000 00000003425 14517741044 0020637 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000047151 14517741044 0022200 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002770 14517741044 0020703 0 ustar 00root root 0000000 0000000 class 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.py 0000664 0000000 0000000 00000004640 14517741044 0021032 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000001005 14517741044 0021677 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000033305 14517741044 0020532 0 ustar 00root root 0000000 0000000 import 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/ 0000775 0000000 0000000 00000000000 14517741044 0015077 5 ustar 00root root 0000000 0000000 flask-login-0.6.3/tests/test_login.py 0000664 0000000 0000000 00000205777 14517741044 0017642 0 ustar 00root root 0000000 0000000 from __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.ini 0000664 0000000 0000000 00000001072 14517741044 0015250 0 ustar 00root root 0000000 0000000 [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