pax_global_header 0000666 0000000 0000000 00000000064 13004165076 0014514 g ustar 00root root 0000000 0000000 52 comment=1dacd1e474528231a6f98b4edf6d52a4c50cda19
flask-login-0.4.0/ 0000775 0000000 0000000 00000000000 13004165076 0013723 5 ustar 00root root 0000000 0000000 flask-login-0.4.0/.gitignore 0000664 0000000 0000000 00000000172 13004165076 0015713 0 ustar 00root root 0000000 0000000 !.gitignore
tests_output/
__pycache__/
.tox
.coverage
*.py[co]
*.egg-info
*.swp
dist/
docs/_build/
virtualenv/
venv/
flask-login-0.4.0/.travis.yml 0000664 0000000 0000000 00000000204 13004165076 0016030 0 ustar 00root root 0000000 0000000 language: python
python:
- 2.6
- 2.7
- 3.3
- 3.4
- 3.5
install:
- python install_requirements.py dev
script: make check
flask-login-0.4.0/CHANGES 0000664 0000000 0000000 00000006600 13004165076 0014720 0 ustar 00root root 0000000 0000000 Flask-Login Changelog
=====================
Here you can see the full list of changes between each Flask-Login release.
Version 0.4.0
-------------
Released on TBD.
- 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.4.0/CONTRIBUTING.md 0000664 0000000 0000000 00000003134 13004165076 0016155 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.4.0/ISSUE_TEMPLATE.md 0000664 0000000 0000000 00000000502 13004165076 0016425 0 ustar 00root root 0000000 0000000 Make sure these boxes are checked before submitting your issue--thank you!
- [ ] Ensure you are using the latest PyPI release.
- [ ] Read the [CHANGES](https://github.com/maxcountryman/flask-login/blob/master/CHANGES) document thoroughly.
- [ ] Provide a clear and simple set of steps to reproduce your issue for others.
flask-login-0.4.0/LICENSE 0000664 0000000 0000000 00000002043 13004165076 0014727 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.4.0/MANIFEST.in 0000664 0000000 0000000 00000000032 13004165076 0015454 0 ustar 00root root 0000000 0000000 include README.md LICENSE
flask-login-0.4.0/Makefile 0000664 0000000 0000000 00000001336 13004165076 0015366 0 ustar 00root root 0000000 0000000 .PHONY: all test clean_coverage clean pep8 pyflakes check
all:
@echo 'test run the unit tests'
@echo 'coverage generate coverage statistics'
@echo 'pep8 check pep8 compliance'
@echo 'pyflakes check for unused imports (requires pyflakes)'
@echo 'check make sure you are ready to commit'
@echo 'clean cleanup the source tree'
test: clean_coverage
@echo 'Running all tests...'
@VERBOSE=1 PATH=${PATH} ./run-tests.sh
clean_coverage:
@rm -f .coverage
clean:
@rm -f flask_login/*.pyc
pep8:
@echo 'Checking pep8 compliance...'
@pep8 flask_login/* test_login.py
pyflakes:
@echo 'Running pyflakes...'
@pyflakes flask_login/* test_login.py
check: clean pep8 pyflakes test
flask-login-0.4.0/README.md 0000664 0000000 0000000 00000010725 13004165076 0015207 0 ustar 00root root 0000000 0000000 # Flask-Login
[](https://travis-ci.org/#!/maxcountryman/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.
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 one of the following commands:
```sh
$ easy_install flask-login
```
or alternatively if you have pip installed:
```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': {'pw': '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
# DO NOT ever store passwords in plaintext and always compare password
# hashes using constant-time comparison!
user.is_authenticated = request.form['pw'] == users[email]['pw']
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 flask.request.form['pw'] == users[email]['pw']:
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'
```
Complete documentation for Flask-Login is available on [ReadTheDocs](https://flask-login.readthedocs.io/en/latest/).
## 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 requirements in `dev-requirements.txt`
4. Submit a pull request after running `make check` (ensure it does not error!)
Please give us adequate time to review your submission. Thanks!
flask-login-0.4.0/dev-py3k-requirements.txt 0000664 0000000 0000000 00000000171 13004165076 0020646 0 ustar 00root root 0000000 0000000 flask==0.10.1
blinker==1.2
coverage==3.6
mock==1.0.1
nose==1.2.1
pep8==1.4.2
pyflakes==1.1.0
yanc==0.2.3
werkzeug==0.9.1
flask-login-0.4.0/dev-requirements.txt 0000664 0000000 0000000 00000000207 13004165076 0017762 0 ustar 00root root 0000000 0000000 flask==0.9
blinker==1.2
coverage==3.7
mock==1.0.1
nose==1.3.0
pep8==1.4.2
pyflakes==1.1.0
unittest2==0.5.1
yanc==0.2.4
werkzeug==0.8.3
flask-login-0.4.0/docs/ 0000775 0000000 0000000 00000000000 13004165076 0014653 5 ustar 00root root 0000000 0000000 flask-login-0.4.0/docs/Makefile 0000664 0000000 0000000 00000011002 13004165076 0016305 0 ustar 00root root 0000000 0000000 # Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
help:
@echo "Please use \`make ' where is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Login.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Login.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Login"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Login"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
make -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
flask-login-0.4.0/docs/_themes/ 0000775 0000000 0000000 00000000000 13004165076 0016277 5 ustar 00root root 0000000 0000000 flask-login-0.4.0/docs/_themes/LICENSE 0000664 0000000 0000000 00000003375 13004165076 0017314 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.4.0/docs/_themes/README 0000664 0000000 0000000 00000002105 13004165076 0017155 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.4.0/docs/_themes/flask/ 0000775 0000000 0000000 00000000000 13004165076 0017377 5 ustar 00root root 0000000 0000000 flask-login-0.4.0/docs/_themes/flask/layout.html 0000664 0000000 0000000 00000001106 13004165076 0021600 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.4.0/docs/_themes/flask/relations.html 0000664 0000000 0000000 00000001116 13004165076 0022264 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.4.0/docs/_themes/flask_small/static/ 0000775 0000000 0000000 00000000000 13004165076 0022056 5 ustar 00root root 0000000 0000000 flask-login-0.4.0/docs/_themes/flask_small/static/flasky.css_t 0000664 0000000 0000000 00000011001 13004165076 0024375 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.4.0/docs/_themes/flask_small/theme.conf 0000664 0000000 0000000 00000000270 13004165076 0022537 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.4.0/docs/_themes/flask_theme_support.py 0000664 0000000 0000000 00000011413 13004165076 0022727 0 ustar 00root root 0000000 0000000 # flasky extensions. flasky pygments style based on tango style
from pygments.style import Style
from pygments.token import Keyword, Name, Comment, String, Error, \
Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
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.4.0/docs/conf.py 0000664 0000000 0000000 00000017055 13004165076 0016162 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# 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 sys, os
# 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 = u'Flask-Login'
copyright = u'2011, Matthew Frazier'
module_path = os.path.join(os.path.dirname(__file__), '..', 'flask_login')
module_path = os.path.abspath(module_path)
about = {}
with open(os.path.join(os.path.dirname(__file__), '..', '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', u'Flask-Login Documentation',
u'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', u'Flask-Login Documentation',
[u'Matthew Frazier'], 1)
]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/3': None,
'http://flask.pocoo.org/docs/': None}
auto_content = 'both'
flask-login-0.4.0/docs/index.rst 0000664 0000000 0000000 00000046061 13004165076 0016523 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
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::
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)
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 `unicode` 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 `unicode` 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 `unicode` - if the ID is natively
an `int` or some other type, you will need to convert it to `unicode`.
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')
# next_is_valid should check if the user has valid
# permission to access the `next` url
if not next_is_valid(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.
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.
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
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` and `is_authenticated` are `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
===========
"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 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 session token instead of the user's ID. For example::
@login_manager.user_loader
def load_user(session_token):
return User.query.filter_by(session_token=session_token).first()
Then the `~UserMixin.get_id` method of your User class would return the session
token instead of the user's ID::
def get_id(self):
return unicode(self.session_token)
This way you are free to change the user's session token 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 session
token 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.
**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:** `None`
`REMEMBER_COOKIE_HTTPONLY` Prevents the "Remember Me" cookie from being
accessed by client-side scripts.
**Default:** `False`
=========================== =================================================
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.
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:: setup_app
.. automethod:: unauthorized
.. automethod:: needs_refresh
.. rubric:: General Configuration
.. automethod:: user_loader
.. automethod:: header_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:: 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_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
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.
.. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/
flask-login-0.4.0/docs/make.bat 0000664 0000000 0000000 00000010651 13004165076 0016263 0 ustar 00root root 0000000 0000000 @ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^` where ^ is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-Login.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-Login.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end
flask-login-0.4.0/flask_login/ 0000775 0000000 0000000 00000000000 13004165076 0016213 5 ustar 00root root 0000000 0000000 flask-login-0.4.0/flask_login/__about__.py 0000664 0000000 0000000 00000000605 13004165076 0020474 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', '4', '0')
__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.4.0/flask_login/__init__.py 0000664 0000000 0000000 00000004003 13004165076 0020321 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
'''
flask_login
-----------
This module provides user session management for Flask. It lets you log
your users in and out in a database-independent manner.
:copyright: (c) 2011 by Matthew Frazier.
:license: MIT/X11, see LICENSE for more details.
'''
from .config import (COOKIE_NAME, COOKIE_DURATION, COOKIE_SECURE,
COOKIE_HTTPONLY, LOGIN_MESSAGE, LOGIN_MESSAGE_CATEGORY,
REFRESH_MESSAGE, REFRESH_MESSAGE_CATEGORY, ID_ATTRIBUTE,
AUTH_HEADER_NAME)
from .login_manager import LoginManager
from .mixins import UserMixin, AnonymousUserMixin
from .signals import (user_logged_in, user_logged_out, user_loaded_from_cookie,
user_loaded_from_header, user_loaded_from_request,
user_login_confirmed, user_unauthorized,
user_needs_refresh, user_accessed, session_protected)
from .utils import (current_user, login_url, login_fresh, login_user,
logout_user, confirm_login, login_required,
fresh_login_required, set_login_view, encode_cookie,
decode_cookie, make_next_param)
__all__ = [
LoginManager.__name__,
UserMixin.__name__,
AnonymousUserMixin.__name__,
'COOKIE_NAME',
'COOKIE_DURATION',
'COOKIE_SECURE',
'COOKIE_HTTPONLY',
'LOGIN_MESSAGE',
'LOGIN_MESSAGE_CATEGORY',
'REFRESH_MESSAGE',
'REFRESH_MESSAGE_CATEGORY',
'ID_ATTRIBUTE',
'AUTH_HEADER_NAME',
'user_logged_in',
'user_logged_out',
'user_loaded_from_cookie',
'user_loaded_from_header',
'user_loaded_from_request',
'user_login_confirmed',
'user_unauthorized',
'user_needs_refresh',
'user_accessed',
'session_protected',
'current_user',
'login_url',
'login_fresh',
'login_user',
'logout_user',
'confirm_login',
'login_required',
'fresh_login_required',
'set_login_view',
'encode_cookie',
'decode_cookie',
'make_next_param',
]
flask-login-0.4.0/flask_login/_compat.py 0000664 0000000 0000000 00000001455 13004165076 0020214 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
'''
flask_login._compat
-------------------
A module providing tools for cross-version compatibility.
'''
import sys
PY2 = sys.version_info[0] == 2
if not PY2: # pragma: no cover
unicode = str # needed for pyflakes in py3
if PY2: # pragma: nocover
from urlparse import urlparse, urlunparse
def iteritems(d):
return d.iteritems()
def itervalues(d):
return d.itervalues()
text_type = unicode
else: # pragma: nocover
from urllib.parse import urlparse, urlunparse
def iteritems(d):
return iter(d.items())
def itervalues(d):
return iter(d.values())
text_type = str
__all__ = [
'PY2',
'unicode',
'urlparse',
'urlunparse',
'iteritems',
'itervalues',
'text_type',
]
flask-login-0.4.0/flask_login/config.py 0000664 0000000 0000000 00000003102 13004165076 0020026 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
'''
flask_login.config
------------------
This module provides default configuration values.
'''
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 ``None``
COOKIE_SECURE = None
#: Whether the "remember me" cookie uses HttpOnly or not; defaults to ``False``
COOKIE_HTTPONLY = False
#: The default flash message to display when users need to log in.
LOGIN_MESSAGE = u'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 = u'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 unicode 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 = set(['user_id', 'remember', '_id', '_fresh'])
#: A set of HTTP methods which are exempt from `login_required` and
#: `fresh_login_required`. By default, this is just ``OPTIONS``.
EXEMPT_METHODS = set(['OPTIONS'])
flask-login-0.4.0/flask_login/login_manager.py 0000664 0000000 0000000 00000040107 13004165076 0021371 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
'''
flask_login.login_manager
-------------------------
The LoginManager class.
'''
import warnings
from datetime import datetime
from flask import (_request_ctx_stack, abort, current_app, flash, redirect,
request, session)
from ._compat import text_type
from .config import (COOKIE_NAME, COOKIE_DURATION, COOKIE_SECURE,
COOKIE_HTTPONLY, LOGIN_MESSAGE, LOGIN_MESSAGE_CATEGORY,
REFRESH_MESSAGE, REFRESH_MESSAGE_CATEGORY, ID_ATTRIBUTE,
AUTH_HEADER_NAME, SESSION_KEYS)
from .mixins import AnonymousUserMixin
from .signals import (user_loaded_from_cookie, user_loaded_from_header,
user_loaded_from_request, user_unauthorized,
user_needs_refresh, user_accessed, session_protected)
from .utils import (_get_user, login_url, _create_identifier,
_user_context_processor, encode_cookie, decode_cookie)
class LoginManager(object):
'''
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.user_callback = None
self.unauthorized_callback = None
self.needs_refresh_callback = None
self.id_attribute = ID_ATTRIBUTE
self.header_callback = None
self.request_callback = None
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.
'''
warnings.warn('Warning setup_app is deprecated. Please use init_app.',
DeprecationWarning)
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)
self._login_disabled = app.config.get('LOGIN_DISABLED', False)
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.)
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)
return redirect(login_url(login_view, request.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 ``unicode``) 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 callback
def header_loader(self, callback):
'''
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
'''
self.header_callback = callback
return 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 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.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)
return redirect(login_url(self.refresh_view, request.url))
def reload_user(self, user=None):
ctx = _request_ctx_stack.top
if user is None:
user_id = session.get('user_id')
if user_id is None:
ctx.user = self.anonymous_user()
else:
if self.user_callback is None:
raise Exception(
"No user_loader has been installed for this "
"LoginManager. Add one with the "
"'LoginManager.user_loader' decorator.")
user = self.user_callback(user_id)
if user is None:
ctx.user = self.anonymous_user()
else:
ctx.user = user
else:
ctx.user = user
def _load_user(self):
'''Loads user from session or remember_me cookie as applicable'''
user_accessed.send(current_app._get_current_object())
# first check SESSION_PROTECTION
config = current_app.config
if config.get('SESSION_PROTECTION', self.session_protection):
deleted = self._session_protection()
if deleted:
return self.reload_user()
# If a remember cookie is set, and the session is not, move the
# cookie user ID to the session.
#
# However, the session may have been set if the user has been
# logged out on this request, 'remember' would be set to clear,
# so we should check for that and not restore the session.
is_missing_user_id = 'user_id' not in session
if is_missing_user_id:
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:
return self._load_from_cookie(request.cookies[cookie_name])
elif self.request_callback:
return self._load_from_request(request)
elif header_name in request.headers:
return self._load_from_header(request.headers[header_name])
return self.reload_user()
def _session_protection(self):
sess = session._get_current_object()
ident = _create_identifier()
app = current_app._get_current_object()
mode = app.config.get('SESSION_PROTECTION', self.session_protection)
# 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:
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_from_cookie(self, cookie):
user_id = decode_cookie(cookie)
if user_id is not None:
session['user_id'] = user_id
session['_fresh'] = False
self.reload_user()
if _request_ctx_stack.top.user is not None:
app = current_app._get_current_object()
user_loaded_from_cookie.send(app, user=_get_user())
def _load_from_header(self, header):
user = None
if self.header_callback:
user = self.header_callback(header)
if user is not None:
self.reload_user(user=user)
app = current_app._get_current_object()
user_loaded_from_header.send(app, user=_get_user())
else:
self.reload_user()
def _load_from_request(self, request):
user = None
if self.request_callback:
user = self.request_callback(request)
if user is not None:
self.reload_user(user=user)
app = current_app._get_current_object()
user_loaded_from_request.send(app, user=_get_user())
else:
self.reload_user()
def _update_remember_cookie(self, response):
# Don't modify the session unless there's something to do.
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)
duration = config.get('REMEMBER_COOKIE_DURATION', COOKIE_DURATION)
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)
# prepare data
data = encode_cookie(text_type(session['user_id']))
try:
expires = datetime.utcnow() + duration
except TypeError:
raise Exception('REMEMBER_COOKIE_DURATION must be a ' +
'datetime.timedelta, instead got: {0}'.format(
duration))
# actually set it
response.set_cookie(cookie_name,
value=data,
expires=expires,
domain=domain,
path=path,
secure=secure,
httponly=httponly)
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)
flask-login-0.4.0/flask_login/mixins.py 0000664 0000000 0000000 00000003307 13004165076 0020077 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
'''
flask_login.mixins
------------------
This module provides mixin objects.
'''
from ._compat import PY2, text_type
class UserMixin(object):
'''
This provides default implementations for the methods that Flask-Login
expects user objects to have.
'''
if not PY2: # pragma: no cover
# 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 True
@property
def is_anonymous(self):
return False
def get_id(self):
try:
return text_type(self.id)
except AttributeError:
raise NotImplementedError('No `id` attribute - override `get_id`')
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(object):
'''
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.4.0/flask_login/signals.py 0000664 0000000 0000000 00000004257 13004165076 0020235 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
'''
flask_login.signals
-------------------
This module provides signals to get notified when Flask-Login performs
certain actions.
'''
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')
flask-login-0.4.0/flask_login/utils.py 0000664 0000000 0000000 00000025712 13004165076 0017734 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
'''
flask_login.utils
-----------------
General utilities.
'''
import hmac
from hashlib import sha512
from functools import wraps
from werkzeug.local import LocalProxy
from werkzeug.security import safe_str_cmp
from werkzeug.urls import url_decode, url_encode
from flask import (_request_ctx_stack, current_app, request, session, url_for,
has_request_context)
from ._compat import text_type, urlparse, urlunparse
from .config import COOKIE_NAME, EXEMPT_METHODS
from .signals import user_logged_in, user_logged_out, 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):
'''
This will encode a ``unicode`` value into a cookie, and sign that cookie
with the app's secret key.
:param payload: The value to encode, as `unicode`.
:type payload: unicode
'''
return u'{0}|{1}'.format(payload, _cookie_digest(payload))
def decode_cookie(cookie):
'''
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
'''
try:
payload, digest = cookie.rsplit(u'|', 1)
if hasattr(digest, 'decode'):
digest = digest.decode('ascii') # pragma: no cover
except ValueError:
return
if safe_str_cmp(_cookie_digest(payload), 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 = urlparse(login_url)
c = urlparse(current_url)
if (not l.scheme or l.scheme == c.scheme) and \
(not l.netloc or l.netloc == c.netloc):
return urlunparse(('', '', c.path, c.params, c.query, ''))
return current_url
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.
: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
'''
if login_view.startswith(('https://', 'http://', '/')):
base = login_view
else:
base = url_for(login_view)
if next_url is None:
return base
parts = list(urlparse(base))
md = url_decode(parts[4])
md[next_field] = make_next_param(base, next_url)
parts[4] = url_encode(md, sort=True)
return urlunparse(parts)
def login_fresh():
'''
This returns ``True`` if the current login is fresh.
'''
return session.get('_fresh', False)
def login_user(user, remember=False, 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 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'] = _create_identifier()
if remember:
session['remember'] = 'set'
_request_ctx_stack.top.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')
cookie_name = current_app.config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
if cookie_name in request.cookies:
session['remember'] = 'clear'
user_logged_out.send(current_app._get_current_object(), user=user)
current_app.login_manager.reload_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'] = _create_identifier()
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:
return func(*args, **kwargs)
elif current_app.login_manager._login_disabled:
return func(*args, **kwargs)
elif not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
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 configutation 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:
return func(*args, **kwargs)
elif current_app.login_manager._login_disabled:
return func(*args, **kwargs)
elif not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
elif not login_fresh():
return current_app.login_manager.needs_refresh()
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() and not hasattr(_request_ctx_stack.top, 'user'):
current_app.login_manager._load_user()
return getattr(_request_ctx_stack.top, 'user', 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 = '{0}|{1}'.format(_get_remote_addr(), user_agent)
if str is bytes:
base = text_type(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, text_type): # pragma: no cover
key = key.encode('latin1') # ensure bytes
return key
flask-login-0.4.0/install_requirements.py 0000664 0000000 0000000 00000000673 13004165076 0020554 0 ustar 00root root 0000000 0000000 import sys
import os
if sys.version_info >= (3, 3):
requirements = "py3k-requirements.txt"
elif (2, 6) <= sys.version_info < (3, 0):
requirements = "requirements.txt"
else:
raise AssertionError("only support 2.6, 2.7, 3.3")
is_dev = sys.argv[1] == "dev" if len(sys.argv) > 1 else False
if __name__ == "__main__":
if is_dev:
requirements = "dev-%s" % requirements
os.system("pip install -r %s" % requirements)
flask-login-0.4.0/requirements.txt 0000664 0000000 0000000 00000000033 13004165076 0017203 0 ustar 00root root 0000000 0000000 flask==0.9
werkzeug==0.8.3
flask-login-0.4.0/run-tests.sh 0000775 0000000 0000000 00000001502 13004165076 0016224 0 ustar 00root root 0000000 0000000 #!/bin/bash
OUTPUT_PATH=$(pwd)/tests_output
function log() {
echo "$@" | tee -a $OUTPUT_PATH/test.log
}
rm -rf $OUTPUT_PATH
mkdir -p $OUTPUT_PATH
NOSETEST_OPTIONS="-d"
if [ -n "$VERBOSE" ]; then
NOSETEST_OPTIONS="$NOSETEST_OPTIONS --verbose"
fi
if [ -z "$NOCOLOR" ]; then
NOSETEST_OPTIONS="$NOSETEST_OPTIONS --with-yanc --yanc-color=on"
fi
if [ -n "$OPTIONS" ]; then
NOSETEST_OPTIONS="$NOSETEST_OPTIONS $OPTIONS"
fi
if [ -n "$TESTS" ]; then
NOSETEST_OPTIONS="$NOSETEST_OPTIONS $TESTS"
else
NOSETEST_OPTIONS="$NOSETEST_OPTIONS --with-coverage --cover-min-percentage=100 --cover-package=flask_login"
fi
log "Running tests..."
nosetests $NOSETEST_OPTIONS 2>&1 | tee -a $OUTPUT_PATH/test.log
ret=${PIPESTATUS[0]}
echo
case "$ret" in
0) log -e "SUCCESS" ;;
*) log -e "FAILURE" ;;
esac
exit $ret
flask-login-0.4.0/setup.cfg 0000664 0000000 0000000 00000000034 13004165076 0015541 0 ustar 00root root 0000000 0000000 [bdist_wheel]
universal = 1
flask-login-0.4.0/setup.py 0000664 0000000 0000000 00000003562 13004165076 0015443 0 ustar 00root root 0000000 0000000 '''
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.
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.
Links
`````
* `documentation `_
* `development version `_
'''
import os
import sys
from setuptools import setup
about = {}
with open('flask_login/__about__.py') as f:
exec(f.read(), about)
if sys.argv[-1] == 'test':
status = os.system('make check')
status >>= 8
sys.exit(status)
setup(name=about['__title__'],
version=about['__version__'],
url=about['__url__'],
license=about['__license__'],
author=about['__author__'],
author_email=about['__author_email__'],
description=about['__description__'],
long_description=__doc__,
packages=['flask_login'],
zip_safe=False,
platforms='any',
install_requires=['Flask'],
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
'Topic :: Software Development :: Libraries :: Python Modules'
])
flask-login-0.4.0/test_login.py 0000664 0000000 0000000 00000135675 13004165076 0016465 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
try:
import unittest2 as unittest
except ImportError:
import unittest
import base64
import collections
from datetime import timedelta, datetime
from contextlib import contextmanager
from mock import ANY
from werkzeug import __version__ as werkzeug_version
from flask import (
Flask,
Blueprint,
Response,
session,
get_flashed_messages,
)
from flask.views import MethodView
from flask_login import (LoginManager, UserMixin, AnonymousUserMixin,
current_user, login_user, logout_user, user_logged_in,
user_logged_out, user_loaded_from_cookie,
user_login_confirmed, user_loaded_from_header,
user_loaded_from_request, user_unauthorized,
user_needs_refresh, make_next_param, login_url,
login_fresh, login_required, session_protected,
fresh_login_required, confirm_login, encode_cookie,
decode_cookie, set_login_view, user_accessed)
from flask_login.utils import _secret_key, _user_context_processor
# be compatible with py3k
if str is not bytes:
unicode = str
@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(object):
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 = '{0} signals were fired'.format(len(self.heard))
raise AssertionError(msg)
elif self.heard[0] != (args, kwargs):
msg = 'One signal was heard, but with incorrect arguments: '\
'Got ({0}) expected ({1}, {2})'
raise AssertionError(msg.format(self.heard[0], args, kwargs))
def assert_heard_none(self, *args, **kwargs):
''' The signal fired no times '''
if len(self.heard) >= 1:
msg = '{0} signals were fired'.format(len(self.heard))
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(u'Notch', 1)
steve = User(u'Steve', 2)
creeper = User(u'Creeper', 3, False)
germanjapanese = User(u'Müller', u'佐藤') # Unicode user_id
USERS = {1: notch, 2: steve, 3: creeper, u'佐藤': germanjapanese}
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)
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)
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_login_disabled_is_set(self):
login_manager = LoginManager(self.app, add_context_processor=True)
self.assertFalse(login_manager._login_disabled)
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.reload_user()
expected_exception_message = 'No user_loader has been installed'
self.assertTrue(
str(cm.exception).startswith(expected_exception_message))
class MethodViewLoginTestCase(unittest.TestCase):
def setUp(self):
self.app = Flask(__name__)
self.login_manager = LoginManager()
self.login_manager.init_app(self.app)
self.login_manager._login_disabled = False
class SecretEndpoint(MethodView):
decorators = [
login_required,
fresh_login_required,
]
def options(self):
return u''
def get(self):
return u''
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.login_manager._login_disabled = False
@self.app.route('/')
def index():
return u'Welcome!'
@self.app.route('/secret')
def secret():
return self.login_manager.unauthorized()
@self.app.route('/login-notch')
def login_notch():
return unicode(login_user(notch))
@self.app.route('/login-notch-remember')
def login_notch_remember():
return unicode(login_user(notch, remember=True))
@self.app.route('/login-notch-permanent')
def login_notch_permanent():
session.permanent = True
return unicode(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 u''
@self.app.route('/username')
def username():
if current_user.is_authenticated:
return current_user.name
return u'Anonymous'
@self.app.route('/is-fresh')
def is_fresh():
return unicode(login_fresh())
@self.app.route('/logout')
def logout():
return unicode(logout_user())
@self.login_manager.user_loader
def load_user(user_id):
return USERS[int(user_id)]
@self.login_manager.header_loader
def load_user_from_header(header_value):
if header_value.startswith('Basic '):
header_value = header_value.replace('Basic ', '', 1)
try:
user_id = base64.b64decode(header_value)
except TypeError:
pass
return USERS.get(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 unicode(u'modified=%s' % 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 _get_remember_cookie(self, test_client):
our_cookies = test_client.cookie_jar._cookies['localhost.local']['/']
return our_cookies[self.remember_cookie_name]
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 permament 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(u'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, u'Notch')
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, u'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, u'Creeper')
def test_login_user_with_header(self):
user_id = 2
user_name = USERS[user_id].name
self.login_manager.request_callback = None
with self.app.test_client() as c:
basic_fmt = 'Basic {0}'
decoded = bytes.decode(base64.b64encode(str.encode(str(user_id))))
headers = [('Authorization', basic_fmt.format(decoded))]
result = c.get('/username', headers=headers)
self.assertEqual(user_name, result.data.decode('utf-8'))
def test_login_invalid_user_with_header(self):
user_id = 9000
user_name = u'Anonymous'
self.login_manager.request_callback = None
with self.app.test_client() as c:
basic_fmt = 'Basic {0}'
decoded = bytes.decode(base64.b64encode(str.encode(str(user_id))))
headers = [('Authorization', basic_fmt.format(decoded))]
result = c.get('/username', headers=headers)
self.assertEqual(user_name, result.data.decode('utf-8'))
def test_login_user_with_request(self):
user_id = 2
user_name = USERS[user_id].name
with self.app.test_client() as c:
url = '/username?user_id={user_id}'.format(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 = u'Anonymous'
with self.app.test_client() as c:
url = '/username?user_id={user_id}'.format(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 = u'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 == u'Log in!':
return u'Einloggen'
self.login_manager.login_view = '/login'
self.login_manager.localize_callback = _gettext
self.login_manager.login_message = u'Log in!'
expected_message = u'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(u'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,
'http://localhost/login?next=%2Fsecret')
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 u'Access Granted'
@first.route('/protected')
@login_required
def first_protected():
return u'Access Granted'
@second.route('/protected')
@login_required
def second_protected():
return u'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 = ('http://localhost/'
'app_login?next=%2Fprotected')
self.assertEqual(result.location, expected)
result = c.get('/first/protected')
self.assertEqual(result.status_code, 302)
expected = ('http://localhost/'
'first_login?next=%2Ffirst%2Fprotected')
self.assertEqual(result.location, expected)
result = c.get('/second/protected')
self.assertEqual(result.status_code, 302)
expected = ('http://localhost/'
'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 u'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 = 'http://localhost/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(u'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'), u'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(u'Anonymous', result.data.decode('utf-8'))
def test_authentication_is_fresh(self):
with self.app.test_client() as c:
c.get('/login-notch-remember')
result = c.get('/is-fresh')
self.assertEqual(u'True', 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)
result = c.get('/username')
self.assertEqual(u'Notch', 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'
with self.app.test_client() as c:
c.get('/login-notch-remember')
# TODO: Is there a better way to test this?
self.assertIn(domain, c.cookie_jar._cookies,
'Custom domain not found as cookie domain')
domain_cookie = c.cookie_jar._cookies[domain]
self.assertIn(path, domain_cookie,
'Custom path not found as cookie path')
path_cookie = domain_cookie[path]
self.assertIn(name, path_cookie,
'Custom name not found as cookie name')
cookie = path_cookie[name]
expiration_date = datetime.utcfromtimestamp(cookie.expires)
expected_date = datetime.utcnow() + duration
difference = expected_date - expiration_date
fail_msg = 'The expiration date {0} was far from the expected {1}'
fail_msg = fail_msg.format(expiration_date, expected_date)
self.assertLess(difference, timedelta(seconds=10), fail_msg)
self.assertGreater(difference, timedelta(seconds=-10), fail_msg)
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_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 = 'Exception: ' \
'REMEMBER_COOKIE_DURATION must be a datetime.timedelta, ' \
'instead got: 123'
self.assertIn(expected_exception_message, str(cm.exception))
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(u'False', c.get('/is-fresh').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(u'Notch', result.data.decode('utf-8'))
result = c.get('/username',
headers=[('X-Forwarded-For', '10.1.1.1')])
self.assertEqual(u'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(u'Notch', result.data.decode('utf-8'))
result = c.get('/username',
headers=[('X-Forwarded-For', '10.1.1.1, 10.1.1.2')])
self.assertEqual(u'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_header_fired(self):
user_id = 1
user_name = USERS[user_id].name
self.login_manager.request_callback = None
with self.app.test_client() as c:
with listen_to(user_loaded_from_header) as listener:
headers = [
(
'Authorization',
'Basic %s' % (
bytes.decode(
base64.b64encode(str.encode(str(user_id))))
),
)
]
result = c.get('/username', headers=headers)
self.assertEqual(user_name, result.data.decode('utf-8'))
listener.assert_heard_one(self.app, user=USERS[user_id])
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 = '/username?user_id={user_id}'.format(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'), u'Anonymous')
def test_needs_refresh_uses_handler(self):
@self.login_manager.needs_refresh_handler
def _on_refresh():
return u'Needs Refresh!'
with self.app.test_client() as c:
c.get('/login-notch-remember')
result = c.get('/needs-refresh')
self.assertEqual(u'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 = u'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 == u'Refresh':
return u'Aktualisieren'
self.login_manager.refresh_view = '/refresh_view'
self.login_manager.localize_callback = _gettext
self.login_manager.needs_refresh_message = u'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(u'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 = 'http://localhost/refresh-view?next=%2Fneeds-refresh'
self.assertEqual(result.location, expected)
def test_confirm_login(self):
with self.app.test_client() as c:
c.get('/login-notch-remember')
self._delete_session(c)
self.assertEqual(u'False', c.get('/is-fresh').data.decode('utf-8'))
c.get('/confirm-login')
self.assertEqual(u'True', c.get('/is-fresh').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.assertEquals(
u'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)
#
# 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(u'Notch', username_result.data.decode('utf-8'))
fresh_result = c.get('/is-fresh')
self.assertEqual(u'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(u'Notch', username_result.data.decode('utf-8'))
fresh_result = c.get('/is-fresh')
self.assertEqual(u'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(u'Notch', username_result.data.decode('utf-8'))
fresh_result = c.get('/is-fresh')
self.assertEqual(u'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(u'Notch', username_result.data.decode('utf-8'))
fresh_result = c.get('/is-fresh')
self.assertEqual(u'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(u'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'), u'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'), u'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'), u'Notch')
#
# View Decorators
#
def test_login_required_decorator(self):
@self.app.route('/protected')
@login_required
def protected():
return u'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(u'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 u'Access Granted'
self.app.login_manager._login_disabled = True
with self.app.test_client() as c:
result = c.get('/protected')
self.assertIn(u'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(u'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(u'Access Granted',
refreshed_result.data.decode('utf-8'))
#
# Misc
#
@unittest.skipIf(werkzeug_version.startswith("0.9"),
"wait for upstream implementing RFC 5987")
def test_chinese_user_agent(self):
with self.app.test_client() as c:
result = c.get('/', headers=[('User-Agent', u'中文')])
self.assertEqual(u'Welcome!', result.data.decode('utf-8'))
@unittest.skipIf(werkzeug_version.startswith("0.9"),
"wait for upstream implementing RFC 5987")
def test_russian_cp1251_user_agent(self):
with self.app.test_client() as c:
headers = [('User-Agent', u'ЯЙЮя'.encode('cp1251'))]
response = c.get('/', headers=headers)
self.assertEqual(response.data.decode('utf-8'), u'Welcome!')
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 TestLoginUrlGeneration(unittest.TestCase):
def test_make_next_param(self):
self.assertEqual('/profile',
make_next_param('/login', 'http://localhost/profile'))
self.assertEqual('http://localhost/profile',
make_next_param('https://localhost/login',
'http://localhost/profile'))
self.assertEqual('http://localhost/profile',
make_next_param('http://accounts.localhost/login',
'http://localhost/profile'))
def test_login_url_generation(self):
PROTECTED = 'http://localhost/protected'
self.assertEqual('/login?n=%2Fprotected', login_url('/login',
PROTECTED, 'n'))
self.assertEqual('/login?next=%2Fprotected', login_url('/login',
PROTECTED))
expected = 'https://auth.localhost/login' + \
'?next=http%3A%2F%2Flocalhost%2Fprotected'
self.assertEqual(expected,
login_url('https://auth.localhost/login', PROTECTED))
self.assertEqual('/login?affil=cgnu&next=%2Fprotected',
login_url('/login?affil=cgnu', PROTECTED))
def test_login_url_generation_with_view(self):
app = Flask(__name__)
login_manager = LoginManager()
login_manager.init_app(app)
@app.route('/login')
def login():
return ''
with 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 = u'0e9e6e9855fbe6df7906ec4737578a1d491b38d3fd5246c1561016e189d6516'
h2 = u'043286501ca43257c938e60aad77acec5ce916b94ca9d00c0bb6f9883ae4b82'
h3 = u'ae'
COOKIE = u'1|' + h1 + h2 + h3
with app.test_request_context():
self.assertEqual(COOKIE, encode_cookie(u'1'))
self.assertEqual(u'1', decode_cookie(COOKIE))
self.assertIsNone(decode_cookie(u'Foo|BAD_BASH'))
self.assertIsNone(decode_cookie(u'no bar'))
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):
def __init__(self, id):
self.id = id
class ExplicitIdUser(UserMixin):
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(u'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 == u'1')
self.assertTrue(first != u'1')
def test_hashable(self):
self.assertTrue(isinstance(UserMixin(), collections.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.login_manager._login_disabled = False
@self.app.route('/')
def index():
return u'Welcome!'
@self.app.route('/login-germanjapanese-remember')
def login_germanjapanese_remember():
return unicode(login_user(germanjapanese, remember=True))
@self.app.route('/username')
def username():
if current_user.is_authenticated:
return current_user.name
return u'Anonymous'
@self.app.route('/userid')
def user_id():
if current_user.is_authenticated:
return current_user.id
return u'wrong_id'
@self.login_manager.user_loader
def load_user(user_id):
return USERS[unicode(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 permament 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(u'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(u'佐藤', result.data.decode('utf-8'))
flask-login-0.4.0/tox.ini 0000664 0000000 0000000 00000000571 13004165076 0015241 0 ustar 00root root 0000000 0000000 [tox]
envlist = py26,py27,py33,pypy
[testenv:py26]
deps = -r{toxinidir}/dev-requirements.txt
commands =
make check
[testenv:py27]
deps = -r{toxinidir}/dev-requirements.txt
commands =
make check
[testenv:pypy]
deps = -r{toxinidir}/dev-requirements.txt
commands =
make check
[testenv:py33]
deps = -r{toxinidir}/dev-py3k-requirements.txt
commands =
make check