Compare commits

...
146 Commits
Author SHA1 Message Date
Matt Wright 8b1ab24341 Bump version number to 1.7.5 2015-12-02 17:20:46 -05:00
Matt Wright ea77940ffc Merge pull request #454 from jonafato/changelog-1.7.5
Add changelog for bugfix version 1.7.5
2015-12-02 17:19:37 -05:00
Jon Banafato 2adc572e80 Add changelog for bugfix version 1.7.5 2015-12-02 17:13:32 -05:00
Jon Banafato 2ac4f373ab Merge pull request #438 from jonafato/is_active-fix
Fix use of is_active for Flask-Login>=0.3.0
2015-12-02 16:42:06 -05:00
Jon Banafato 132ced5675 Merge pull request #442 from nfvs/migrate_travis_containers
Migrate to Travis container-based infrastructure, optimize build with pip caching.
2015-11-18 11:29:14 -05:00
Nuno Santos aeb5ffbb1d Enable pip and apt cache in Travis for faster build times. 2015-11-12 16:07:00 +01:00
Nuno Santos c2ee2e2629 Migrate to Travis container-based infrastructure.
Advantages are listed here:
http://docs.travis-ci.com/user/migrating-from-legacy
2015-11-12 14:51:07 +01:00
Jon Banafato 8005709997 Fix use of is_active for Flask-Login>=0.3.0 2015-11-11 16:02:41 -05:00
Matt Wright 2abdaa873a Merge pull request #437 from jonafato/tox-test-improvements
Fix tests, add py35
2015-11-11 15:58:29 -05:00
Jon Banafato 4508c717fb Fix tests, add py35
- Add Python 3.5 support on travis

- pytest-cache has been merged into pytest [0] and is no longer
  separately maintained, so pytest-cache-related requirements are being
  updated.

- Flask-Mongoengine latest stable is broken on Python 2.6 (but fixed in
  master [1]), so its version is being restricted for now.

[0] http://pytest.org/latest/changelog.html#id3 [1]
https://github.com/MongoEngine/flask-mongoengine/pull/174
2015-11-03 14:14:11 -05:00
Matt Wright 9583dc3e63 Merge pull request #417 from ialex/develop
Fixes #416 Changing is_authenticated from function to property & updating Flask-…
2015-11-03 10:46:57 -05:00
Alejandro Villanueva 745fae1ab1 Adding >0.4 to flask-loginrequirement 2015-09-24 10:03:46 -05:00
Alejandro Villanueva 7e8551763e Changing is_authenticated from function to property & updating Flask-Login>=0.3.0 2015-09-11 10:31:01 -05:00
Matt Wright 4049c06203 Merge pull request #366 from covertgeek/develop
Modified check_token function to handle list-type JSON post
2015-07-10 14:11:15 -04:00
Matt Wright 3b678b872f Merge pull request #400 from feliciaan/patch-1
Fixes a small spelling error
2015-07-10 14:07:15 -04:00
Derek Rushing 17a79ed6a3 Merge branch 'develop' of https://github.com/mattupstate/flask-security into develop 2015-07-06 06:51:20 -05:00
Derek Rushing d65c3cadcd Merge branch 'master' of https://github.com/mattupstate/flask-security into develop 2015-07-06 06:47:23 -05:00
Feliciaan De Palmenaer bf01ad0727 Fixes a small spelling error 2015-06-28 20:17:23 +02:00
Matt Wright cbd0db7c39 Merge pull request #398 from jonafato/bcrypt-hotfix
Restrict bcrypt to <2.0.0
2015-06-24 22:23:36 -05:00
Jon Banafato 398f5c920b Restrict bcrypt to <2.0.0
As of 2.0.0, passlib no longer correctly identifies bcrypt as bcrypt
(instead, it mistakenly applies pybcrypt logic to bcrypt). This results
in all Python 3 logic involving bcrypt failing. As a hotfix, we should
require users to be on a version of bcrypt that passlib can handle a fix
can be pushed into passlib.
2015-06-23 13:23:07 -04:00
Matt Wright 33252ae178 Merge pull request #389 from nickretallack/develop
Reset Password Fixes
2015-05-26 14:59:30 -04:00
Nick Retallack c10c9050c7 test: reset password on a user who has no password
The user may have been invited via a social network or an invitation
system.
2015-05-11 23:22:30 -07:00
Nick Retallack 5697ff80c3 ignore the eggs readme 2015-05-11 23:16:04 -07:00
Nick Retallack 4411470202 test: invalidate used password reset tokens
Also pep8 compliance and suggested changes.
2015-05-11 23:12:05 -07:00
Nick Retallack a0e2037747 invalidate password reset tokens when the passwords changes
Check that the previous password is the same as it was when this
password reset request was generated.
2015-05-11 21:52:57 -07:00
Nick Retallack 7884d637c5 prevent password reset from breaking if you have no password
If you've just been invited, or are using social auth, you have no
password set, so the reset password feature causes a crash.  This
doesn't need to happen.
2015-05-11 21:52:57 -07:00
Matt Wright 8a14abaa1e Fix failing test 2015-05-02 14:57:34 -04:00
Matt Wright 6b55e9613a Merge pull request #255 from nfvs/unauthorized_override
Allow overriding of an unauthorized callback.
2015-05-02 14:45:57 -04:00
Matt Wright d08aac6d35 Fix pymongo version issue 2015-05-02 14:34:31 -04:00
Nuno Santos 10fd1844d8 Allow overriding of unauthorized callback.
Related to issue #255.
2015-05-02 20:17:38 +02:00
Matt Wright 72d3a4b5e3 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2015-05-02 13:57:29 -04:00
Matt Wright 6541640ee8 Merge pull request #373 from lnielsen/anonymoususer
Add support for custom AnonymousUser class.
2015-05-02 13:57:20 -04:00
Matt Wright 9cda8baff3 Fix #367 2015-05-02 13:55:05 -04:00
Matt Wright d907e5b890 Merge pull request #363 from jeffwidman/patch-1
"overrided" --> "overriden"
2015-05-02 13:50:39 -04:00
Matt Wright 8a62b5f193 Merge pull request #361 from nfvs/add_headers_to_auth_required
Include WWW-Authenticate headers in @auth_required.
2015-05-02 13:50:23 -04:00
Matt Wright 2e08ec87a6 Merge pull request #352 from fuhrysteve/develop
X-Forwarded-For can contain multiple IP addresses
2015-05-02 13:46:21 -04:00
Matt Wright 79fe821c80 Merge pull request #351 from denizdogan/master
Add documentation about SECURITY_MSG configuration variables.
2015-05-02 13:45:13 -04:00
Matt Wright 4d3c1c0bdc Merge pull request #347 from Jaza/slash-url-suffix
re #343: Add slash before or after token in flask-security URLs correctly
2015-05-02 13:43:36 -04:00
Matt Wright cd8982fa65 Merge pull request #342 from alexef/patch-1
Fail silently for get_user(None)
2015-05-02 13:30:01 -04:00
Matt Wright c8a3549e2d Merge pull request #331 from Diaoul/patch-2
Fix ActivateUserCommand docstring
2015-05-02 13:29:21 -04:00
Matt Wright 03d9cf2f0d Merge pull request #330 from Diaoul/patch-1
Fix RemoveRoleCommand docstring
2015-05-02 13:29:07 -04:00
Matt Wright e4d9d3ad17 Merge pull request #322 from waltaskew/develop
Add configuration for token expiration
2015-05-02 13:27:11 -04:00
Matt Wright f2a5e4b614 Normalize import paths. Fixes #313 2015-05-02 13:25:26 -04:00
Matt Wright 916f5ee012 Use StringField instead of TextField. Fixes #312 2015-05-02 13:05:46 -04:00
Matt Wright bc1f5dd7f9 Stricter tests for signals and a small docs update. Fixes #308 2015-05-02 12:59:02 -04:00
Matt Wright 4659d10c5c forgot password endpoint should be for anonymous users only. Fixes #291 2015-05-02 12:11:05 -04:00
Lars Holm Nielsen a4581681e5 Fix PEP8 error. 2015-03-06 13:09:05 +01:00
Lars Holm Nielsen 248ea5d272 Custom AnonymousUser support.
(addresses #362)
2015-03-06 12:45:17 +01:00
Derek Rushing 4debc8d102 Modified check_token function to account for multiple objects being posted via JSON. Resolves issue with it throwing an error when it encounters a list instead of a dict type. 2015-02-18 10:20:22 -06:00
Jeff Widman fccaccd282 "overrided" --> "overriden"
Per http://english.stackexchange.com/questions/75786/overrode-vs-overridden
2015-02-02 23:34:52 -08:00
Nuno Santos 3681823fcf Include WWW-Authenticate headers in @auth_required.
When using @http_auth_required, the WWW-Authenticate header is included,
but when using @auth_required('basic'), it is not. This change includes
that header in every @auth_required call that contains the 'basic'
method.
2015-01-30 11:27:53 +01:00
Stephen J. FuhryandStephen J. Fuhry 923ad720a1 X-Forwarded-For can contain multiple IP addresses
From the nginx docs:
http://nginx.org/en/docs/http/ngx_http_proxy_module.html
> $proxy_add_x_forwarded_for
> the “X-Forwarded-For” client request header field with the $remote_addr
> variable appended to it, separated by a comma. If the “X-Forwarded-For”
> field is not present in the client request header, the
> $proxy_add_x_forwarded_for variable is equal to the $remote_addr
> variable.

Use the last IP address in X-Forwarded-For. For this to work properly
behind a trusted proxy, you must be using ProxyFix as described in the
flask & werkzeug documentation.
2014-12-29 08:31:19 -05:00
Deniz Dogan f4d758f561 Add documentation about SECURITY_MSG configuration variables. 2014-12-21 15:20:48 +01:00
Jeremy Epstein 665b164618 split docstring into multiple lines to make travis CI happy 2014-11-28 13:50:25 +11:00
Jeremy Epstein 4d70f016ad re #343: Add slash before or after token in flask-security URLs correctly 2014-11-28 10:36:31 +11:00
Alex Eftimie 7e4fc94601 Fail silently for get_user(None)
get_user(identifier) checks if the identifier is a number by trying to convert it to int. This works for strings, but in a particular case, when identifier is None, it fails. Checking for both TypeError and ValueError fixes it.
2014-11-19 14:11:58 +02:00
Antoine Bertin 6cfe662dc6 Fix ActivateUserCommand docstring 2014-10-21 11:26:17 +02:00
Antoine Bertin 8c45271bf9 Fix RemoveRoleCommand docstring 2014-10-21 10:27:17 +02:00
Matt Wright c7d0ea9cce Add additional item to CHANGES 2014-10-13 13:47:35 -04:00
Matt Wright 94c7c09dc2 Bump version number to 1.7.4 2014-10-13 13:38:44 -04:00
Matt Wright f6405797f1 Update CHANGES 2014-10-13 13:38:16 -04:00
Matt Wright 2cffb6634d Fix case sensitivity when searching for users by email address. Fixes #323. 2014-10-13 13:34:11 -04:00
Matt Wright 824a52b883 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2014-10-13 13:25:08 -04:00
Matt Wright fe7e4c4afc Add additional steps to the test_change_hash_type and make change to verify_and_update_password to make the test pass. Fixes #328. 2014-10-13 13:25:01 -04:00
Matt Wright 591bc27a5e Merge pull request #318 from boydgreenfield/develop
Prevent open redirects when a malformed URL is passed to ?next=
2014-10-13 11:54:32 -04:00
waltaskew 897b2fceab Add configuration for token expiration 2014-10-01 15:59:28 -04:00
Nick Greenfield 5bc37add88 Update to use (url_next.netloc or url_next.scheme) in the validate_redirect_url open redirect patch. 2014-10-01 09:49:00 -07:00
Nick Greenfield 8b036f2a3e Prevent open redirects when a malformed URL is passed to ?next=
Example: "/login?next=http:///google.com" (note 3rd slash)
2014-09-26 11:08:58 -07:00
Matt Wright 76ad77a233 Compare a string (not bytes) for PY3 support 2014-09-17 11:39:19 -04:00
Matt Wright 679cee7969 Add default/global context processor. Fixes #306 2014-09-17 11:27:44 -04:00
Matt Wright 3d7b97ac31 Forgot password form should not validate if user has not confirmed their email address yet. Fixes #298 2014-09-17 11:21:31 -04:00
Matt Wright 3a0af73231 Merge pull request #281 from sjml/patch-1
Fixing typo in documentation
2014-09-17 10:03:53 -04:00
Matt Wright 3458391791 Merge pull request #284 from tony/patch-1
Typo
2014-09-17 10:02:27 -04:00
Matt Wright 37908ca335 Merge pull request #303 from graup/patch-1
Docs: Fixed typo and made punctuation more consistent.
2014-09-17 10:02:16 -04:00
Matt Wright f83cd56ff2 Merge pull request #311 from moorereason/develop
Fix small issues in docs
2014-09-17 10:01:57 -04:00
Cameron Moore c10ec29fe5 Fix small issues in docs 2014-09-07 21:37:33 -05:00
Matt Wright d2fe7aefcb Merge pull request #307 from mickey06/develop
Save changes to db after removal of role from user
2014-08-29 12:19:26 -04:00
Matt Wright 708ddeb6c4 Merge pull request #309 from gregeinfrank/develop
Fix two typos in docstrings
2014-08-29 12:15:20 -04:00
Greg Einfrank 2aeee348d4 Fix two typos in docstrings 2014-08-27 23:37:58 -04:00
Khalil El Kouhen 52b177cd2e Save changes to db after removal of role from user 2014-08-26 16:43:23 +01:00
Paul Grau dab2fc8c8b Docs: Fixed typo and made punctuation more consistent. 2014-08-19 18:01:03 +09:00
Matt Wright 31e3ab5470 Merge pull request #289 from scollinson/fix_menu_typo
Fix a typo in the menu template
2014-08-13 15:44:20 -04:00
Matt Wright 6bdcd2930d Merge pull request #278 from dokterbob/post_register_redirect
Enable ‘next’ redirection after registration.
2014-07-29 13:23:42 -04:00
Sam Collinson 1076887900 fix typo in menu template 2014-07-21 19:07:12 +12:00
Tony Narlock 285fe888da Typo
s/tempalte/template
2014-07-16 14:35:47 -07:00
Shane Liesegang bf139847cb Fixing typo in documentation
Just a small usage error. :)
2014-07-11 23:45:00 -07:00
Mathijs de Bruin 15c9ef1d07 Attempt to fix tests. 2014-07-03 15:48:55 +02:00
Mathijs de Bruin d1ec38ea18 Keep value of ‘next’ in menu links.
This allows moving from the login to registration (and vice versa) while keeping the redirection URL, enabling a much smoother user experience.
2014-07-03 15:22:56 +02:00
Mathijs de Bruin 577a3a8110 Enable ‘next’ redirection after registration. 2014-07-03 15:21:03 +02:00
Matt Wright 7b474efd7a Bump version number to 1.7.3 2014-06-10 13:05:24 -04:00
Matt Wright f12bc17e7e Merge branch 'develop' 2014-06-10 13:05:12 -04:00
Matt Wright 999f882f61 Update CHANGES 2014-06-10 13:05:06 -04:00
Matt Wright a140c01b57 Add python3.4 to travis and tox 2014-06-10 12:40:12 -04:00
Matt Wright 76cf3eaf6a Do not expose user info in /reset responses. Fixes #249 2014-06-10 12:24:19 -04:00
Matt Wright a6b5d3053c Use safe_str_cmp when evaluating tokens. Fixes #252 2014-06-10 12:14:58 -04:00
Matt Wright 0a48997fdd Improve encoding of strings. Addresses #231 and #253 2014-06-10 11:47:35 -04:00
Matt Wright 96f1b3e0d1 Fix tests to pass python 3 2014-06-10 11:12:40 -04:00
Matt Wright 0facdaacd9 Make validate_redirect_url smarter. Fixes #261. 2014-06-10 10:48:56 -04:00
Matt Wright 9cadf855a4 Adjust POST_LOGIN_VIEW and POST_LOGOUT_VIEW test 2014-06-10 10:42:28 -04:00
Matt Wright f387759cee Bump version number to 1.7.2 2014-05-06 14:05:27 -04:00
Matt Wright faa9f65596 Update CHANGES 2014-05-06 14:04:44 -04:00
Matt Wright 3d5b37cef2 Polish for static analysis 2014-05-06 13:14:25 -04:00
Matt Wright d185407ac4 Add mock X-Forwarded-For header in trackable tests 2014-05-06 13:14:14 -04:00
Matt Wright 58b7fa8e2e Check X-Forwarded-For header value when tracking IP addresses. Fixes #234 2014-05-06 12:35:50 -04:00
Matt Wright 721b985bc4 Something about a bug... 2014-05-06 11:59:28 -04:00
Matt Wright b08d7c2085 Clean up tests with some pytest marking 2014-05-06 11:58:39 -04:00
Matt Wright fff71e3e04 Polish 2014-05-06 07:57:00 -04:00
Matt Wright 546680a94b Polish 2014-03-14 15:33:25 -04:00
Matt Wright 00e121c869 Remove duplicated line 2014-03-14 15:32:39 -04:00
Matt Wright 7098a6757c Update build files 2014-03-14 15:30:30 -04:00
Matt Wright 510d1356a2 A bunch of adjustments to satisfy existing tests and even some new ones 2014-03-14 15:26:53 -04:00
Matt Wright e5111dbb0c Add moar tests! 2014-03-14 15:26:20 -04:00
Matt Wright 44b91a351a Update gitignore 2014-03-13 18:33:22 -04:00
Matt Wright 4b58a00abd Update tox.ini and MANIFEST.in 2014-03-13 18:32:39 -04:00
Matt Wright f3014d01df Convert all tests to use pytest. Phew! 2014-03-13 18:28:25 -04:00
Matt Wright ed645b51f2 Fix confirmation endpoint to flash an "already confirmed" message if user attempts to confirm after already being confirmed 2014-03-13 17:18:39 -04:00
Matt Wright bc60c021a3 Fix password_changed signal to behave like the other signals. Fixes #222 2014-03-13 09:35:54 -04:00
Matt Wright 9f00613906 Merge pull request #225 from nfvs/fix_emails_with_celery_link
Fixed Celery link in "Emails with Celery".
2014-03-13 09:09:39 -04:00
Nuno Santos 0f872af03a Fixed Celery link in "Emails with Celery". 2014-02-27 16:45:25 +01:00
Matt Wright 1d3a75d77b Merge pull request #223 from ahtik/issue-verify_password
verify_password in 1.7.0 was no longer behaving like in 1.6.9
2014-02-20 10:10:44 -05:00
Ahti Kitsik 1395df334e Changing verify_password so it works like verify_and_update_password. Currently verify_password was not only creating a hmac hash but also encrypting (encrypt_password is first hmac-signing and then encrypting).
Removed unneccessary and wrong tests.
2014-02-20 16:46:49 +02:00
Matt Wright 0268a2d568 Merge pull request #217 from swarmer/logout-fix
Make logout view do nothing if the user is already logged out
2014-02-04 07:24:41 -08:00
Anton Barkovsky 09f6f64e5b Make logout view do nothing if the user is already logged out 2014-02-04 16:51:00 +03:00
Matt Wright 8e8d581f0d Add images to README.rst 2014-01-24 14:17:07 -05:00
Matt Wright 65b454ca7e Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2014-01-24 14:09:46 -05:00
Matt Wright 2006d2ae3d Add coverage and coveralls to .travis.yml 2014-01-24 14:09:39 -05:00
Matt Wright c0673c1239 Merge pull request #180 from pythonhub/develop
Allow overwritten on render_template to use Flask-Themes based app #179
2014-01-24 08:15:00 -08:00
Bruno Rocha 922a038d28 Merge https://github.com/mattupstate/flask-security into develop 2014-01-24 14:06:03 -02:00
Matt Wright 668b4ee651 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2014-01-24 10:32:09 -05:00
Matt Wright f854c24094 Merge pull request #205 from HereLabsInc/catch_decoding_errors
catch possible TypeError and ValueError from serializer
2014-01-24 07:31:51 -08:00
Matt Wright d314e54680 Merge branch 'develop' of github.com:mattupstate/flask-security into develop 2014-01-24 10:25:01 -05:00
Matt Wright 9400032a1f Merge pull request #214 from kleink/develop
Update script.py for Python 2's print statement vs. Python 3's function.
2014-01-24 07:24:55 -08:00
Matt Wright 2e57734d1f Add @anonymous_user_required to register endpoint. Fixes #212 2014-01-24 10:23:42 -05:00
Klaus Klein c658ee4500 Update script.py for Python 2's print statement vs. Python 3's function. 2014-01-24 10:21:41 +01:00
Jameson 45c8951877 passes tests 2014-01-24 04:41:41 +00:00
Jameson b4d1a7c921 update to make test run red 2014-01-23 23:33:11 +00:00
Jameson f47fce9365 add test to show TypeError can occur 2014-01-23 23:26:18 +00:00
Jameson b2174bf035 catch possible TypeError and ValueError from serializer 2014-01-23 23:26:18 +00:00
Matt Wright 316d945d96 Bump version number to 1.7.1 2014-01-14 10:41:53 -05:00
Matt Wright 6eb77c06ad Update CHANGES 2014-01-14 10:41:38 -05:00
Matt Wright 35fd08772b Add configured password hash test back and fix bug with checking passwords 2014-01-14 10:34:57 -05:00
Bruno Rocha 01a4199a16 Merge https://github.com/mattupstate/flask-security into develop
Conflicts:
	flask_security/views.py
2013-12-23 13:55:13 -02:00
Bruno Rocha 910d80d511 Fix version to be able to merge 2013-12-23 13:46:14 -02:00
Bruno Rocha 6494275c8c typo 2013-11-16 22:01:49 -02:00
Bruno Rocha aa80198047 Try to fix a problem with version installed 2013-11-16 21:58:27 -02:00
Bruno Rocha 23b128bcbd Allow overwritten on render_template to use Flask-Themes based app 2013-11-07 04:57:54 -02:00
73 changed files with 2725 additions and 2333 deletions
+11
View File
@@ -33,3 +33,14 @@ env/
*~ *~
*.db *.db
*cache*
# vim
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
*~
.eggs/README.txt
+22 -4
View File
@@ -4,17 +4,35 @@ python:
- "2.6" - "2.6"
- "2.7" - "2.7"
- "3.3" - "3.3"
- "3.4"
- "3.5"
- "pypy" - "pypy"
sudo: false
cache:
- apt
- pip
install: install:
- pip install . --quiet - pip install -r requirements.txt -r requirements-dev.txt -e .
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib --quiet --use-mirrors; fi" - pip install coverage coveralls
- pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Peewee bcrypt --quiet - "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib; fi"
addons:
apt:
sources:
- deadsnakes
packages:
- python3.5
services: services:
- mongodb - mongodb
script: nosetests script: py.test --cov flask_security --cov-report term-missing --pep8 --flakes
after_script:
- coveralls
branches: branches:
only: only:
+1
View File
@@ -36,3 +36,4 @@ Rotem Yaari
Srijan Choudhary Srijan Choudhary
Tristan Escalada Tristan Escalada
Vadim Kotov Vadim Kotov
Walt Askew
+60
View File
@@ -3,6 +3,66 @@ Flask-Security Changelog
Here you can see the full list of changes between each Flask-Security release. Here you can see the full list of changes between each Flask-Security release.
Version 1.7.5
-------------
Released December 2nd 2015
- Added `SECURITY_TOKEN_MAX_AGE` configuration setting
- Fixed calls to `SQLAlchemyUserDatastore.get_user(None)` (this now returns `False` instead of raising a `TypeError`
- Fixed URL generation adding extra slashes in some cases (see GitHub #343)
- Fixed handling of trackable IP addresses when the `X-Forwarded-For` header contains multiple values
- Include WWW-Authenticate headers in `@auth_required` authentication checks
- Fixed error when `check_token` function is used with a json list
- Added support for custom `AnonymousUser` classes
- Restricted `forgot_password` endpoint to anonymous users
- Allowed unauthorized callback to be overridden
- Fixed issue where passwords cannot be reset if currently set to `None`
- Ensured that password reset tokens are invalidated after use
- Updated `is_authenticated` and `is_active` functions to support Flask-Login changes
- Various documentation improvements
Version 1.7.4
-------------
Released October 13th 2014
- Fixed a bug related to changing existing passwords from plaintext to hashed
- Fixed a bug in form validation that did not enforce case insensivitiy
- Fixed a bug with validating redirects
Version 1.7.3
-------------
Released June 10th 2014
- Fixed a bug where redirection to `SECURITY_POST_LOGIN_VIEW` was not respected
- Fixed string encoding in various places to be friendly to unicode
- Now using `werkzeug.security.safe_str_cmp` to check tokens
- Removed user information from JSON output on `/reset` responses
- Added Python 3.4 support
Version 1.7.2
-------------
Released May 6th 2014
- Updated IP tracking to check for `X-Forwarded-For` header
- Fixed a bug regarding the re-hashing of passwords with a new algorithm
- Fixed a bug regarding the `password_changed` signal.
Version 1.7.1
-------------
Released January 14th 2014
- Fixed a bug where passwords would fail to verify when specifying a password hash algorithm
Version 1.7.0 Version 1.7.0
------------- -------------
+6 -2
View File
@@ -1,3 +1,7 @@
include CHANGES LICENSE include README.rst
recursive-include tests *.py include requirements.txt
include requirements-dev.txt
include tox.ini
recursive-include flask_security/templates *.* recursive-include flask_security/templates *.*
recursive-include tests *
recursive-exclude tests/__pycache__ *
+16 -1
View File
@@ -3,6 +3,21 @@ Flask-Security
.. image:: https://secure.travis-ci.org/mattupstate/flask-security.png?branch=develop .. image:: https://secure.travis-ci.org/mattupstate/flask-security.png?branch=develop
.. image:: https://pypip.in/v/Flask-Security/badge.png
:target: https://pypi.python.org/pypi/Flask-Security/
:alt: Latest Version
.. image:: https://coveralls.io/repos/mattupstate/flask-security/badge.png?branch=develop
:target: https://coveralls.io/r/mattupstate/flask-security
.. image:: https://pypip.in/d/Flask-Security/badge.png
:target: https://pypi.python.org/pypi//Flask-Security/
:alt: Downloads
.. image:: https://pypip.in/license/Flask-Security/badge.png
:target: https://pypi.python.org/pypi/Flask-Security/
:alt: License
Flask-Security quickly adds security features to your Flask application. Flask-Security quickly adds security features to your Flask application.
Resources Resources
@@ -12,4 +27,4 @@ Resources
- `Issue Tracker <http://github.com/mattupstate/flask-security/issues>`_ - `Issue Tracker <http://github.com/mattupstate/flask-security/issues>`_
- `Code <http://github.com/mattupstate/flask-security/>`_ - `Code <http://github.com/mattupstate/flask-security/>`_
- `Development Version - `Development Version
<http://github.com/mattupstate/flask-security/zipball/develop#egg=Flask-Security-dev>`_ <http://github.com/mattupstate/flask-security/zipball/develop#egg=Flask-Security-dev>`_
+14 -19
View File
@@ -87,43 +87,38 @@ sends the following signals.
.. data:: user_registered .. data:: user_registered
Sent when a user registers on the site. It is passed a dict with Sent when a user registers on the site. In addition to the app (which is the
the `user` and `confirm_token`, the user being logged in and the sender), it is passed `user` and `confirm_token` arguments.
(if so configured) the confirmation token issued.
.. data:: user_confirmed .. data:: user_confirmed
Sent when a user is confirmed. It is passed `user`, which is the Sent when a user is confirmed. In addition to the app (which is the
user being confirmed. sender), it is passed a `user` argument.
.. data:: confirm_instructions_sent .. data:: confirm_instructions_sent
Sent when a user requests confirmation instructions. It is passed Sent when a user requests confirmation instructions. In addition to the app
the `user`. (which is the sender), it is passed a `user` argument.
.. data:: login_instructions_sent .. data:: login_instructions_sent
Sent when passwordless login is used and user logs in. It is passed Sent when passwordless login is used and user logs in. In addition to the app
a dict with the `user` and `login_token`, the user being logged in (which is the sender), it is passed `user` and `login_token` arguments.
and the (if so configured) the login token issued.
.. data:: password_reset .. data:: password_reset
Sent when a user completes a password reset. It is passed the Sent when a user completes a password reset. In addition to the app (which is
`user`. the sender), it is passed a `user` argument.
.. data:: password_changed .. data:: password_changed
Sent when a user completes a password change. It is passed the Sent when a user completes a password change. In addition to the app (which is
`user`. the sender), it is passed a `user` argument.
.. data:: reset_password_instructions_sent .. data:: reset_password_instructions_sent
Sent when a user requests a password reset. It is passed a dict Sent when a user requests a password reset. In addition to the app (which is
with the `user` and `token`, the user being logged in and the sender), it is passed `user` and `token` arguments.
the (if so configured) the reset token issued.
All signals are also passed a `app` keyword argument, which is the
current application.
.. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/ .. _Flask documentation on signals: http://flask.pocoo.org/docs/signals/
+1 -1
View File
@@ -49,7 +49,7 @@ copyright = u'2012, Matt Wright'
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '1.7.0' version = '1.7.5'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = version release = version
+62 -13
View File
@@ -15,7 +15,7 @@ Core
``SECURITY_URL_PREFIX`` Specifies the URL prefix for the ``SECURITY_URL_PREFIX`` Specifies the URL prefix for the
Flask-Security blueprint. Defaults to Flask-Security blueprint. Defaults to
``None``. ``None``.
``SECURITY_FLASH_MESSAGES`` Specifies wether or not to flash ``SECURITY_FLASH_MESSAGES`` Specifies whether or not to flash
messages during security procedures. messages during security procedures.
Defaults to ``True``. Defaults to ``True``.
``SECURITY_PASSWORD_HASH`` Specifies the password hash algorithm to ``SECURITY_PASSWORD_HASH`` Specifies the password hash algorithm to
@@ -23,7 +23,10 @@ Core
passwords. Recommended values for passwords. Recommended values for
production systems are ``bcrypt``, production systems are ``bcrypt``,
``sha512_crypt``, or ``pbkdf2_sha512``. ``sha512_crypt``, or ``pbkdf2_sha512``.
Defaults to ``plaintext``. Defaults to ``plaintext``. Note:
``bcrypt>=2.0.0`` is not currently
supported. If ``bcrypt`` is preferred,
please use ``bcrypt<2.0``.
``SECURITY_PASSWORD_SALT`` Specifies the HMAC salt. This is only ``SECURITY_PASSWORD_SALT`` Specifies the HMAC salt. This is only
used if the password hash type is set used if the password hash type is set
to something other than plain text. to something other than plain text.
@@ -31,12 +34,16 @@ Core
``SECURITY_EMAIL_SENDER`` Specifies the email address to send ``SECURITY_EMAIL_SENDER`` Specifies the email address to send
emails as. Defaults to emails as. Defaults to
``no-reply@localhost``. ``no-reply@localhost``.
``SECURITY_TOKEN_AUTHENTICATION_KEY`` Specifies the query sting parameter to ``SECURITY_TOKEN_AUTHENTICATION_KEY`` Specifies the query string parameter to
read when using token authentication. read when using token authentication.
Defaults to ``auth_token``. Defaults to ``auth_token``.
``SECURITY_TOKEN_AUTHENTICATION_HEADER`` Specifies the HTTP header to read when ``SECURITY_TOKEN_AUTHENTICATION_HEADER`` Specifies the HTTP header to read when
using token authentication. Defaults to using token authentication. Defaults to
``Authentication-Token``. ``Authentication-Token``.
``SECURITY_TOKEN_MAX_AGE`` Specifies the number of seconds before
an authentication token expires.
Defaults to None, meaning the token
never expires.
``SECURITY_DEFAULT_HTTP_AUTH_REALM`` Specifies the default authentication ``SECURITY_DEFAULT_HTTP_AUTH_REALM`` Specifies the default authentication
realm when using basic HTTP auth. realm when using basic HTTP auth.
Defaults to ``Login Required`` Defaults to ``Login Required``
@@ -69,37 +76,37 @@ URLs and Views
``SECURITY_CONFIRM_ERROR_VIEW`` Specifies the view to redirect to if a ``SECURITY_CONFIRM_ERROR_VIEW`` Specifies the view to redirect to if a
confirmation error occurs. This value can be set confirmation error occurs. This value can be set
to a URL or an endpoint name. If this value is to a URL or an endpoint name. If this value is
``None`` the user is presented the default view ``None``, the user is presented the default view
to resend a confirmation link. Defaults to to resend a confirmation link. Defaults to
``None``. ``None``.
``SECURITY_POST_REGISTER_VIEW`` Specifies the view to redirect to after a user ``SECURITY_POST_REGISTER_VIEW`` Specifies the view to redirect to after a user
successfully registers. This value can be set to successfully registers. This value can be set to
a URL or an endpoint name. If this value is a URL or an endpoint name. If this value is
``None`` the user is redirected to the value of ``None``, the user is redirected to the value of
``SECURITY_POST_LOGIN_VIEW``. Defaults to ``SECURITY_POST_LOGIN_VIEW``. Defaults to
``None``. ``None``.
``SECURITY_POST_CONFIRM_VIEW`` Specifies the view to redirect to after a user ``SECURITY_POST_CONFIRM_VIEW`` Specifies the view to redirect to after a user
successfully confirms their email. This value successfully confirms their email. This value
can be set to a URL or an endpoint name. If this can be set to a URL or an endpoint name. If this
value is ``None`` the user is redirected to the value is ``None``, the user is redirected to the
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
to ``None``. to ``None``.
``SECURITY_POST_RESET_VIEW`` Specifies the view to redirect to after a user ``SECURITY_POST_RESET_VIEW`` Specifies the view to redirect to after a user
successfully resets their password. This value successfully resets their password. This value
can be set to a URL or an endpoint name. If this can be set to a URL or an endpoint name. If this
value is ``None`` the user is redirected to the value is ``None``, the user is redirected to the
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
to ``None``. to ``None``.
``SECURITY_POST_CHANGE_VIEW`` Specifies the view to redirect to after a user ``SECURITY_POST_CHANGE_VIEW`` Specifies the view to redirect to after a user
successfully changes their password. This value successfully changes their password. This value
can be set to a URL or an endpoint name. If this can be set to a URL or an endpoint name. If this
value is ``None`` the user is redirected to the value is ``None``, the user is redirected to the
value of ``SECURITY_POST_LOGIN_VIEW``. Defaults value of ``SECURITY_POST_LOGIN_VIEW``. Defaults
to ``None``. to ``None``.
``SECURITY_UNAUTHORIZED_VIEW`` Specifies the view to redirect to if a user ``SECURITY_UNAUTHORIZED_VIEW`` Specifies the view to redirect to if a user
attempts to access a URL/endpoint that they do attempts to access a URL/endpoint that they do
not have permission to access. If this value is not have permission to access. If this value is
``None`` the user is presented with a default ``None``, the user is presented with a default
HTTP 403 response. Defaults to ``None``. HTTP 403 response. Defaults to ``None``.
=============================== ================================================ =============================== ================================================
@@ -144,7 +151,7 @@ Feature Flags
========================= ====================================================== ========================= ======================================================
``SECURITY_CONFIRMABLE`` Specifies if users are required to confirm their email ``SECURITY_CONFIRMABLE`` Specifies if users are required to confirm their email
address when registering a new account. If this value address when registering a new account. If this value
is `True` Flask-Security creates an endpoint to handle is `True`, Flask-Security creates an endpoint to handle
confirmations and requests to resend confirmation confirmations and requests to resend confirmation
instructions. The URL for this endpoint is specified instructions. The URL for this endpoint is specified
by the ``SECURITY_CONFIRM_URL`` configuration option. by the ``SECURITY_CONFIRM_URL`` configuration option.
@@ -158,11 +165,12 @@ Feature Flags
specified by the ``SECURITY_RESET_URL`` configuration specified by the ``SECURITY_RESET_URL`` configuration
option. Defaults to ``False``. option. Defaults to ``False``.
``SECURITY_TRACKABLE`` Specifies if Flask-Security should track basic user ``SECURITY_TRACKABLE`` Specifies if Flask-Security should track basic user
login statistics. If set to ``True`` ensure your login statistics. If set to ``True``, ensure your
models have the required fields/attribues. Defaults to models have the required fields/attribues. Be sure to
use `ProxyFix <http://flask.pocoo.org/docs/0.10/deploying/wsgi-standalone/#proxy-setups>` if you are using a proxy. Defaults to
``False`` ``False``
``SECURITY_PASSWORDLESS`` Specifies if Flask-Security should enable the ``SECURITY_PASSWORDLESS`` Specifies if Flask-Security should enable the
passwordless login feature. If set to ``True`` users passwordless login feature. If set to ``True``, users
are not required to enter a password to login but are are not required to enter a password to login but are
sent an email with a login link. This feature is sent an email with a login link. This feature is
experimental and should be used with caution. Defaults experimental and should be used with caution. Defaults
@@ -261,3 +269,44 @@ Miscellaneous
me" value used when logging in me" value used when logging in
a user. Defaults to ``False``. a user. Defaults to ``False``.
============================================= ================================== ============================================= ==================================
Messages
-------------
The following are the messages Flask-Security uses. They are tuples; the first
element is the message and the second element is the error level.
The default messages and error levels can be found in ``core.py``.
* ``SECURITY_MSG_ALREADY_CONFIRMED``
* ``SECURITY_MSG_CONFIRMATION_EXPIRED``
* ``SECURITY_MSG_CONFIRMATION_REQUEST``
* ``SECURITY_MSG_CONFIRMATION_REQUIRED``
* ``SECURITY_MSG_CONFIRM_REGISTRATION``
* ``SECURITY_MSG_DISABLED_ACCOUNT``
* ``SECURITY_MSG_EMAIL_ALREADY_ASSOCIATED``
* ``SECURITY_MSG_EMAIL_CONFIRMED``
* ``SECURITY_MSG_EMAIL_NOT_PROVIDED``
* ``SECURITY_MSG_INVALID_CONFIRMATION_TOKEN``
* ``SECURITY_MSG_INVALID_EMAIL_ADDRESS``
* ``SECURITY_MSG_INVALID_LOGIN_TOKEN``
* ``SECURITY_MSG_INVALID_PASSWORD``
* ``SECURITY_MSG_INVALID_REDIRECT``
* ``SECURITY_MSG_INVALID_RESET_PASSWORD_TOKEN``
* ``SECURITY_MSG_LOGIN``
* ``SECURITY_MSG_LOGIN_EMAIL_SENT``
* ``SECURITY_MSG_LOGIN_EXPIRED``
* ``SECURITY_MSG_PASSWORDLESS_LOGIN_SUCCESSFUL``
* ``SECURITY_MSG_PASSWORD_CHANGE``
* ``SECURITY_MSG_PASSWORD_INVALID_LENGTH``
* ``SECURITY_MSG_PASSWORD_IS_THE_SAME``
* ``SECURITY_MSG_PASSWORD_MISMATCH``
* ``SECURITY_MSG_PASSWORD_NOT_PROVIDED``
* ``SECURITY_MSG_PASSWORD_NOT_SET``
* ``SECURITY_MSG_PASSWORD_RESET``
* ``SECURITY_MSG_PASSWORD_RESET_EXPIRED``
* ``SECURITY_MSG_PASSWORD_RESET_REQUEST``
* ``SECURITY_MSG_REFRESH``
* ``SECURITY_MSG_RETYPE_PASSWORD_MISMATCH``
* ``SECURITY_MSG_UNAUTHORIZED``
* ``SECURITY_MSG_USER_DOES_NOT_EXIST``
+10 -9
View File
@@ -2,7 +2,7 @@ Customizing Views
================= =================
Flask-Security bootstraps your application with various views for handling its Flask-Security bootstraps your application with various views for handling its
configured features to get you up and running as quick as possible. However, configured features to get you up and running as quickly as possible. However,
you'll probably want to change the way these views look to be more in line with you'll probably want to change the way these views look to be more in line with
your application's visual design. your application's visual design.
@@ -36,7 +36,7 @@ Flask application context processor:
* ``<template_name>_form``: A form object for the view * ``<template_name>_form``: A form object for the view
* ``security``: The Flask-Security extension object * ``security``: The Flask-Security extension object
To add more values to the template context you can specify a context processor To add more values to the template context, you can specify a context processor
for all views or a specific view. For example:: for all views or a specific view. For example::
security = Security(app, user_datastore) security = Security(app, user_datastore)
@@ -73,8 +73,8 @@ register form or override validators::
from flask_security.forms import RegisterForm from flask_security.forms import RegisterForm
class ExtendedRegisterForm(RegisterForm): class ExtendedRegisterForm(RegisterForm):
first_name = TextField('First Name', [Required()]) first_name = StringField('First Name', [Required()])
last_name = TextField('Last Name', [Required()]) last_name = StringField('Last Name', [Required()])
security = Security(app, user_datastore, security = Security(app, user_datastore,
register_form=ExtendedRegisterForm) register_form=ExtendedRegisterForm)
@@ -106,7 +106,7 @@ The following is a list of all the available form overrides:
Emails Emails
------ ------
Flask-Security is also packaged with a default tempalte for each email that it Flask-Security is also packaged with a default template for each email that it
may send. Templates are located within the subfolder named ``security/email``. may send. Templates are located within the subfolder named ``security/email``.
The following is a list of email templates: The following is a list of email templates:
@@ -131,7 +131,7 @@ Overriding these templates is simple:
Each template is passed a template context object that includes values for any Each template is passed a template context object that includes values for any
links that are required in the email. If you require more values in the links that are required in the email. If you require more values in the
templates you can specify an email context processor with the templates, you can specify an email context processor with the
``mail_context_processor`` decorator. For example:: ``mail_context_processor`` decorator. For example::
security = Security(app, user_datastore) security = Security(app, user_datastore)
@@ -145,9 +145,9 @@ templates you can specify an email context processor with the
Emails with Celery Emails with Celery
------------------ ------------------
Sometimes it makes sense to send emails via a task queue, such as Sometimes it makes sense to send emails via a task queue, such as `Celery`_.
`Celery<http://www.celeryproject.org/>`_. To delay the sending of emails you can To delay the sending of emails, you can use the ``@security.send_mail_task``
use the ``@security.send_mail_task`` decorator like so:: decorator like so::
# Setup the task # Setup the task
@celery.task @celery.task
@@ -160,3 +160,4 @@ use the ``@security.send_mail_task`` decorator like so::
def delay_security_email(msg): def delay_security_email(msg):
send_security_email.delay(msg) send_security_email.delay(msg)
.. _Celery: http://www.celeryproject.org/
+2 -2
View File
@@ -24,7 +24,7 @@ instance, you may assign roles such as `Admin`, `Editor`, `SuperUser`, or a
combination of said roles to a user. Access control is based on the role name combination of said roles to a user. Access control is based on the role name
and all roles should be uniquely named. This feature is implemented using the and all roles should be uniquely named. This feature is implemented using the
`Flask-Principal`_ extension. If you'd like to implement more granular access `Flask-Principal`_ extension. If you'd like to implement more granular access
control you can refer to the Flask-Princpal `documentation on this topic`_. control, you can refer to the Flask-Principal `documentation on this topic`_.
Password Encryption Password Encryption
@@ -89,7 +89,7 @@ User Registration
Flask-Security comes packaged with a basic user registration view. This view is Flask-Security comes packaged with a basic user registration view. This view is
very simple and new users need only supply an email address and their password. very simple and new users need only supply an email address and their password.
This view can be overrided if your registration process requires more fields. This view can be overridden if your registration process requires more fields.
Login Tracking Login Tracking
+3 -3
View File
@@ -5,7 +5,7 @@ Flask-Security assumes you'll be using libraries such as SQLAlchemy,
MongoEngine or Peewee to define a data model that includes a `User` and MongoEngine or Peewee to define a data model that includes a `User` and
`Role` model. The fields on your models must follow a particular convention `Role` model. The fields on your models must follow a particular convention
depending on the functionality your app requires. Aside from this, you're depending on the functionality your app requires. Aside from this, you're
free to add any additional fields to your model(s) if you want. At the bear free to add any additional fields to your model(s) if you want. At the bare
minimum your `User` and `Role` model should include the following fields: minimum your `User` and `Role` model should include the following fields:
**User** **User**
@@ -32,7 +32,7 @@ Confirmable
^^^^^^^^^^^ ^^^^^^^^^^^
If you enable account confirmation by setting your application's If you enable account confirmation by setting your application's
`SECURITY_CONFIRMABLE` configuration value to `True` your `User` model will `SECURITY_CONFIRMABLE` configuration value to `True`, your `User` model will
require the following additional field: require the following additional field:
* ``confirmed_at`` * ``confirmed_at``
@@ -41,7 +41,7 @@ Trackable
^^^^^^^^^ ^^^^^^^^^
If you enable user tracking by setting your application's `SECURITY_TRACKABLE` If you enable user tracking by setting your application's `SECURITY_TRACKABLE`
configuration value to `True` your `User` model will require the following configuration value to `True`, your `User` model will require the following
additional fields: additional fields:
* ``last_login_at`` * ``last_login_at``
+7 -7
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security flask_security
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
Flask-Security is a Flask extension that aims to add quick and simple Flask-Security is a Flask extension that aims to add quick and simple
security via Flask-Login, Flask-Principal, Flask-WTF, and passlib. security via Flask-Login, Flask-Principal, Flask-WTF, and passlib.
@@ -10,14 +10,14 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
__version__ = '1.7.0'
from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user
from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore
from .decorators import auth_token_required, http_auth_required, \ from .decorators import auth_token_required, http_auth_required, \
login_required, roles_accepted, roles_required, auth_required login_required, roles_accepted, roles_required, auth_required
from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \ from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \
ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm
from .signals import confirm_instructions_sent, password_reset, \ from .signals import confirm_instructions_sent, password_reset, \
reset_password_instructions_sent, user_confirmed, user_registered reset_password_instructions_sent, user_confirmed, user_registered
from .utils import login_user, logout_user, url_for_security from .utils import login_user, logout_user, url_for_security
__version__ = '1.7.5'
+6 -5
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.changeable flask_security.changeable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security recoverable module Flask-Security recoverable module
@@ -29,8 +29,8 @@ def send_password_changed_notice(user):
:param user: The user to send the notice to :param user: The user to send the notice to
""" """
if config_value('SEND_PASSWORD_CHANGE_EMAIL'): if config_value('SEND_PASSWORD_CHANGE_EMAIL'):
send_mail(config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE'), user.email, subject = config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE')
'change_notice', user=user) send_mail(subject, user.email, 'change_notice', user=user)
def change_user_password(user, password): def change_user_password(user, password):
@@ -42,4 +42,5 @@ def change_user_password(user, password):
user.password = encrypt_password(password) user.password = encrypt_password(password)
_datastore.put(user) _datastore.put(user)
send_password_changed_notice(user) send_password_changed_notice(user)
password_changed.send(user, app=app._get_current_object()) password_changed.send(app._get_current_object(),
user=user._get_current_object())
+8 -3
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.confirmable flask_security.confirmable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security confirmable module Flask-Security confirmable module
@@ -58,7 +58,9 @@ def generate_confirmation_token(user):
def requires_confirmation(user): def requires_confirmation(user):
"""Returns `True` if the user requires confirmation.""" """Returns `True` if the user requires confirmation."""
return _security.confirmable and not _security.login_without_confirmation and user.confirmed_at == None return (_security.confirmable and
not _security.login_without_confirmation and
user.confirmed_at is None)
def confirm_email_token_status(token): def confirm_email_token_status(token):
@@ -77,6 +79,9 @@ def confirm_user(user):
:param user: The user to confirm :param user: The user to confirm
""" """
if user.confirmed_at is not None:
return False
user.confirmed_at = datetime.utcnow() user.confirmed_at = datetime.utcnow()
_datastore.put(user) _datastore.put(user)
user_confirmed.send(app._get_current_object(), user=user) user_confirmed.send(app._get_current_object(), user=user)
return True
+118 -64
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.core flask_security.core
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
Flask-Security core module Flask-Security core module
@@ -9,15 +9,16 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
from flask import current_app from flask import current_app, render_template
from flask.ext.login import AnonymousUserMixin, UserMixin as BaseUserMixin, \ from flask_login import AnonymousUserMixin, UserMixin as BaseUserMixin, \
LoginManager, current_user LoginManager, current_user
from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \ from flask_principal import Principal, RoleNeed, UserNeed, Identity, \
identity_loaded identity_loaded
from itsdangerous import URLSafeTimedSerializer from itsdangerous import URLSafeTimedSerializer
from passlib.context import CryptContext from passlib.context import CryptContext
from werkzeug.datastructures import ImmutableList from werkzeug.datastructures import ImmutableList
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from werkzeug.security import safe_str_cmp
from .utils import config_value as cv, get_config, md5, url_for_security, string_types from .utils import config_value as cv, get_config, md5, url_for_security, string_types
from .views import create_blueprint from .views import create_blueprint
@@ -74,6 +75,7 @@ _default_config = {
'EMAIL_SENDER': 'no-reply@localhost', 'EMAIL_SENDER': 'no-reply@localhost',
'TOKEN_AUTHENTICATION_KEY': 'auth_token', 'TOKEN_AUTHENTICATION_KEY': 'auth_token',
'TOKEN_AUTHENTICATION_HEADER': 'Authentication-Token', 'TOKEN_AUTHENTICATION_HEADER': 'Authentication-Token',
'TOKEN_MAX_AGE': None,
'CONFIRM_SALT': 'confirm-salt', 'CONFIRM_SALT': 'confirm-salt',
'RESET_SALT': 'reset-salt', 'RESET_SALT': 'reset-salt',
'LOGIN_SALT': 'login-salt', 'LOGIN_SALT': 'login-salt',
@@ -87,56 +89,92 @@ _default_config = {
'EMAIL_SUBJECT_PASSWORD_NOTICE': 'Your password has been reset', 'EMAIL_SUBJECT_PASSWORD_NOTICE': 'Your password has been reset',
'EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE': 'Your password has been changed', 'EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE': 'Your password has been changed',
'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions', 'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions',
'USER_IDENTITY_ATTRIBUTES': ['email'] 'USER_IDENTITY_ATTRIBUTES': ['email'],
'PASSWORD_SCHEMES': [
'bcrypt',
'des_crypt',
'pbkdf2_sha256',
'pbkdf2_sha512',
'sha256_crypt',
'sha512_crypt',
# And always last one...
'plaintext'
],
'DEPRECATED_PASSWORD_SCHEMES': ['auto']
} }
#: Default Flask-Security messages #: Default Flask-Security messages
_default_messages = { _default_messages = {
'UNAUTHORIZED': ('You do not have permission to view this resource.', 'error'), 'UNAUTHORIZED': (
'CONFIRM_REGISTRATION': ('Thank you. Confirmation instructions have been sent to %(email)s.', 'success'), 'You do not have permission to view this resource.', 'error'),
'EMAIL_CONFIRMED': ('Thank you. Your email has been confirmed.', 'success'), 'CONFIRM_REGISTRATION': (
'ALREADY_CONFIRMED': ('Your email has already been confirmed.', 'info'), 'Thank you. Confirmation instructions have been sent to %(email)s.', 'success'),
'INVALID_CONFIRMATION_TOKEN': ('Invalid confirmation token.', 'error'), 'EMAIL_CONFIRMED': (
'EMAIL_ALREADY_ASSOCIATED': ('%(email)s is already associated with an account.', 'error'), 'Thank you. Your email has been confirmed.', 'success'),
'PASSWORD_MISMATCH': ('Password does not match', 'error'), 'ALREADY_CONFIRMED': (
'RETYPE_PASSWORD_MISMATCH': ('Passwords do not match', 'error'), 'Your email has already been confirmed.', 'info'),
'INVALID_REDIRECT': ('Redirections outside the domain are forbidden', 'error'), 'INVALID_CONFIRMATION_TOKEN': (
'PASSWORD_RESET_REQUEST': ('Instructions to reset your password have been sent to %(email)s.', 'info'), 'Invalid confirmation token.', 'error'),
'PASSWORD_RESET_EXPIRED': ('You did not reset your password within %(within)s. New instructions have been sent to %(email)s.', 'error'), 'EMAIL_ALREADY_ASSOCIATED': (
'INVALID_RESET_PASSWORD_TOKEN': ('Invalid reset password token.', 'error'), '%(email)s is already associated with an account.', 'error'),
'CONFIRMATION_REQUIRED': ('Email requires confirmation.', 'error'), 'PASSWORD_MISMATCH': (
'CONFIRMATION_REQUEST': ('Confirmation instructions have been sent to %(email)s.', 'info'), 'Password does not match', 'error'),
'CONFIRMATION_EXPIRED': ('You did not confirm your email within %(within)s. New instructions to confirm your email have been sent to %(email)s.', 'error'), 'RETYPE_PASSWORD_MISMATCH': (
'LOGIN_EXPIRED': ('You did not login within %(within)s. New instructions to login have been sent to %(email)s.', 'error'), 'Passwords do not match', 'error'),
'LOGIN_EMAIL_SENT': ('Instructions to login have been sent to %(email)s.', 'success'), 'INVALID_REDIRECT': (
'INVALID_LOGIN_TOKEN': ('Invalid login token.', 'error'), 'Redirections outside the domain are forbidden', 'error'),
'DISABLED_ACCOUNT': ('Account is disabled.', 'error'), 'PASSWORD_RESET_REQUEST': (
'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'), 'Instructions to reset your password have been sent to %(email)s.', 'info'),
'INVALID_EMAIL_ADDRESS': ('Invalid email address', 'error'), 'PASSWORD_RESET_EXPIRED': (
'PASSWORD_NOT_PROVIDED': ('Password not provided', 'error'), 'You did not reset your password within %(within)s. New instructions have been sent '
'PASSWORD_NOT_SET': ('No password is set for this user', 'error'), 'to %(email)s.', 'error'),
'PASSWORD_INVALID_LENGTH': ('Password must be at least 6 characters', 'error'), 'INVALID_RESET_PASSWORD_TOKEN': (
'USER_DOES_NOT_EXIST': ('Specified user does not exist', 'error'), 'Invalid reset password token.', 'error'),
'INVALID_PASSWORD': ('Invalid password', 'error'), 'CONFIRMATION_REQUIRED': (
'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'), 'Email requires confirmation.', 'error'),
'PASSWORD_RESET': ('You successfully reset your password and you have been logged in automatically.', 'success'), 'CONFIRMATION_REQUEST': (
'PASSWORD_IS_THE_SAME': ('Your new password must be different than your previous password.', 'error'), 'Confirmation instructions have been sent to %(email)s.', 'info'),
'PASSWORD_CHANGE': ('You successfully changed your password.', 'success'), 'CONFIRMATION_EXPIRED': (
'LOGIN': ('Please log in to access this page.', 'info'), 'You did not confirm your email within %(within)s. New instructions to confirm your email '
'REFRESH': ('Please reauthenticate to access this page.', 'info'), 'have been sent to %(email)s.', 'error'),
'LOGIN_EXPIRED': (
'You did not login within %(within)s. New instructions to login have been sent to '
'%(email)s.', 'error'),
'LOGIN_EMAIL_SENT': (
'Instructions to login have been sent to %(email)s.', 'success'),
'INVALID_LOGIN_TOKEN': (
'Invalid login token.', 'error'),
'DISABLED_ACCOUNT': (
'Account is disabled.', 'error'),
'EMAIL_NOT_PROVIDED': (
'Email not provided', 'error'),
'INVALID_EMAIL_ADDRESS': (
'Invalid email address', 'error'),
'PASSWORD_NOT_PROVIDED': (
'Password not provided', 'error'),
'PASSWORD_NOT_SET': (
'No password is set for this user', 'error'),
'PASSWORD_INVALID_LENGTH': (
'Password must be at least 6 characters', 'error'),
'USER_DOES_NOT_EXIST': (
'Specified user does not exist', 'error'),
'INVALID_PASSWORD': (
'Invalid password', 'error'),
'PASSWORDLESS_LOGIN_SUCCESSFUL': (
'You have successfuly logged in.', 'success'),
'PASSWORD_RESET': (
'You successfully reset your password and you have been logged in automatically.',
'success'),
'PASSWORD_IS_THE_SAME': (
'Your new password must be different than your previous password.', 'error'),
'PASSWORD_CHANGE': (
'You successfully changed your password.', 'success'),
'LOGIN': (
'Please log in to access this page.', 'info'),
'REFRESH': (
'Please reauthenticate to access this page.', 'info'),
} }
_allowed_password_hash_schemes = [
'bcrypt',
'des_crypt',
'pbkdf2_sha256',
'pbkdf2_sha512',
'sha256_crypt',
'sha512_crypt',
# And always last one...
'plaintext'
]
_default_forms = { _default_forms = {
'login_form': LoginForm, 'login_form': LoginForm,
'confirm_register_form': ConfirmRegisterForm, 'confirm_register_form': ConfirmRegisterForm,
@@ -155,17 +193,17 @@ def _user_loader(user_id):
def _token_loader(token): def _token_loader(token):
try: try:
data = _security.remember_token_serializer.loads(token) data = _security.remember_token_serializer.loads(token, max_age=_security.token_max_age)
user = _security.datastore.find_user(id=data[0]) user = _security.datastore.find_user(id=data[0])
if user and md5(user.password) == data[1]: if user and safe_str_cmp(md5(user.password), data[1]):
return user return user
except: except:
pass pass
return AnonymousUser() return _security.login_manager.anonymous_user()
def _identity_loader(): def _identity_loader():
if not isinstance(current_user._get_current_object(), AnonymousUser): if not isinstance(current_user._get_current_object(), AnonymousUserMixin):
identity = Identity(current_user.id) identity = Identity(current_user.id)
return identity return identity
@@ -180,9 +218,9 @@ def _on_identity_loaded(sender, identity):
identity.user = current_user identity.user = current_user
def _get_login_manager(app): def _get_login_manager(app, anonymous_user):
lm = LoginManager() lm = LoginManager()
lm.anonymous_user = AnonymousUser lm.anonymous_user = anonymous_user or AnonymousUser
lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app) lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app)
lm.user_loader(_user_loader) lm.user_loader(_user_loader)
lm.token_loader(_token_loader) lm.token_loader(_token_loader)
@@ -206,10 +244,12 @@ def _get_principal(app):
def _get_pwd_context(app): def _get_pwd_context(app):
pw_hash = cv('PASSWORD_HASH', app=app) pw_hash = cv('PASSWORD_HASH', app=app)
if pw_hash not in _allowed_password_hash_schemes: schemes = cv('PASSWORD_SCHEMES', app=app)
allowed = ', '.join(_allowed_password_hash_schemes[:-1]) + ' and ' + _allowed_password_hash_schemes[-1] deprecated = cv('DEPRECATED_PASSWORD_SCHEMES', app=app)
if pw_hash not in schemes:
allowed = (', '.join(schemes[:-1]) + ' and ' + schemes[-1])
raise ValueError("Invalid hash scheme %r. Allowed values are %s" % (pw_hash, allowed)) raise ValueError("Invalid hash scheme %r. Allowed values are %s" % (pw_hash, allowed))
return CryptContext(schemes=_allowed_password_hash_schemes, default=pw_hash) return CryptContext(schemes=schemes, default=pw_hash, deprecated=deprecated)
def _get_serializer(app, name): def _get_serializer(app, name):
@@ -218,14 +258,14 @@ def _get_serializer(app, name):
return URLSafeTimedSerializer(secret_key=secret_key, salt=salt) return URLSafeTimedSerializer(secret_key=secret_key, salt=salt)
def _get_state(app, datastore, **kwargs): def _get_state(app, datastore, anonymous_user=None, **kwargs):
for key, value in get_config(app).items(): for key, value in get_config(app).items():
kwargs[key.lower()] = value kwargs[key.lower()] = value
kwargs.update(dict( kwargs.update(dict(
app=app, app=app,
datastore=datastore, datastore=datastore,
login_manager=_get_login_manager(app), login_manager=_get_login_manager(app, anonymous_user),
principal=_get_principal(app), principal=_get_principal(app),
pwd_context=_get_pwd_context(app), pwd_context=_get_pwd_context(app),
remember_token_serializer=_get_serializer(app, 'remember'), remember_token_serializer=_get_serializer(app, 'remember'),
@@ -233,7 +273,8 @@ def _get_state(app, datastore, **kwargs):
reset_serializer=_get_serializer(app, 'reset'), reset_serializer=_get_serializer(app, 'reset'),
confirm_serializer=_get_serializer(app, 'confirm'), confirm_serializer=_get_serializer(app, 'confirm'),
_context_processors={}, _context_processors={},
_send_mail_task=None _send_mail_task=None,
_unauthorized_callback=None
)) ))
for key, value in _default_forms.items(): for key, value in _default_forms.items():
@@ -257,10 +298,14 @@ class RoleMixin(object):
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)
def __hash__(self):
return hash(self.name)
class UserMixin(BaseUserMixin): class UserMixin(BaseUserMixin):
"""Mixin for `User` model definitions""" """Mixin for `User` model definitions"""
@property
def is_active(self): def is_active(self):
"""Returns `True` if the user is active.""" """Returns `True` if the user is active."""
return self.active return self.active
@@ -302,7 +347,7 @@ class _SecurityState(object):
fn not in group and group.append(fn) fn not in group and group.append(fn)
def _run_ctx_processor(self, endpoint): def _run_ctx_processor(self, endpoint):
rv, fns = {}, [] rv = {}
for g in [None, endpoint]: for g in [None, endpoint]:
for fn in self._context_processors.setdefault(g, []): for fn in self._context_processors.setdefault(g, []):
rv.update(fn()) rv.update(fn())
@@ -338,6 +383,9 @@ class _SecurityState(object):
def send_mail_task(self, fn): def send_mail_task(self, fn):
self._send_mail_task = fn self._send_mail_task = fn
def unauthorized_handler(self, fn):
self._unauthorized_callback = fn
class Security(object): class Security(object):
"""The :class:`Security` class initializes the Flask-Security extension. """The :class:`Security` class initializes the Flask-Security extension.
@@ -356,7 +404,8 @@ class Security(object):
login_form=None, confirm_register_form=None, login_form=None, confirm_register_form=None,
register_form=None, forgot_password_form=None, register_form=None, forgot_password_form=None,
reset_password_form=None, change_password_form=None, reset_password_form=None, change_password_form=None,
send_confirmation_form=None, passwordless_login_form=None): send_confirmation_form=None, passwordless_login_form=None,
anonymous_user=None):
"""Initializes the Flask-Security extension for the specified """Initializes the Flask-Security extension for the specified
application and datastore implentation. application and datastore implentation.
@@ -382,15 +431,20 @@ class Security(object):
reset_password_form=reset_password_form, reset_password_form=reset_password_form,
change_password_form=change_password_form, change_password_form=change_password_form,
send_confirmation_form=send_confirmation_form, send_confirmation_form=send_confirmation_form,
passwordless_login_form=passwordless_login_form) passwordless_login_form=passwordless_login_form,
anonymous_user=anonymous_user)
if register_blueprint: if register_blueprint:
app.register_blueprint(create_blueprint(state, __name__)) app.register_blueprint(create_blueprint(state, __name__))
app.context_processor(_context_processor) app.context_processor(_context_processor)
state.render_template = self.render_template
app.extensions['security'] = state app.extensions['security'] = state
return state return state
def render_template(self, *args, **kwargs):
return render_template(*args, **kwargs)
def __getattr__(self, name): def __getattr__(self, name):
return getattr(self._state, name, None) return getattr(self._state, name, None)
+20 -17
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.datastore flask_security.datastore
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
This module contains an user datastore classes. This module contains an user datastore classes.
@@ -85,7 +85,7 @@ class UserDatastore(object):
return kwargs return kwargs
def get_user(self, id_or_email): def get_user(self, id_or_email):
"""Returns a user matching the specified ID or email address""" """Returns a user matching the specified ID or email address."""
raise NotImplementedError raise NotImplementedError
def find_user(self, *args, **kwargs): def find_user(self, *args, **kwargs):
@@ -97,7 +97,7 @@ class UserDatastore(object):
raise NotImplementedError raise NotImplementedError
def add_role_to_user(self, user, role): def add_role_to_user(self, user, role):
"""Adds a role tp a user """Adds a role to a user.
:param user: The user to manipulate :param user: The user to manipulate
:param role: The role to add to the user :param role: The role to add to the user
@@ -110,7 +110,7 @@ class UserDatastore(object):
return False return False
def remove_role_from_user(self, user, role): def remove_role_from_user(self, user, role):
"""Removes a role from a user """Removes a role from a user.
:param user: The user to manipulate :param user: The user to manipulate
:param role: The role to remove from the user :param role: The role to remove from the user
@@ -120,6 +120,7 @@ class UserDatastore(object):
if role in user.roles: if role in user.roles:
rv = True rv = True
user.roles.remove(role) user.roles.remove(role)
self.put(user)
return rv return rv
def toggle_active(self, user): def toggle_active(self, user):
@@ -155,7 +156,7 @@ class UserDatastore(object):
def find_or_create_role(self, name, **kwargs): def find_or_create_role(self, name, **kwargs):
"""Returns a role matching the given name or creates it with any """Returns a role matching the given name or creates it with any
additionally provided parameters additionally provided parameters.
""" """
kwargs["name"] = name kwargs["name"] = name
return self.find_role(name) or self.create_role(**kwargs) return self.find_role(name) or self.create_role(**kwargs)
@@ -167,7 +168,7 @@ class UserDatastore(object):
return self.put(user) return self.put(user)
def delete_user(self, user): def delete_user(self, user):
"""Delete the specified user """Deletes the specified user.
:param user: The user to delete :param user: The user to delete
""" """
@@ -194,7 +195,7 @@ class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore):
def _is_numeric(self, value): def _is_numeric(self, value):
try: try:
int(value) int(value)
except ValueError: except (TypeError, ValueError):
return False return False
return True return True
@@ -237,17 +238,18 @@ class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore):
query = QCombination(QCombination.AND, queries) query = QCombination(QCombination.AND, queries)
try: try:
return self.user_model.objects(query).first() return self.user_model.objects(query).first()
except ValidationError: except ValidationError: # pragma: no cover
return None return None
def find_role(self, role): def find_role(self, role):
return self.role_model.objects(name=role).first() return self.role_model.objects(name=role).first()
def add_role_to_user(self, user, role): # TODO: Not sure why this was added but tests pass without it
rv = super(MongoEngineUserDatastore, self).add_role_to_user(user, role) # def add_role_to_user(self, user, role):
if rv: # rv = super(MongoEngineUserDatastore, self).add_role_to_user(user, role)
self.put(user) # if rv:
return rv # self.put(user)
# return rv
class PeeweeUserDatastore(PeeweeDatastore, UserDatastore): class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
@@ -295,10 +297,11 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
user = self.put(user) user = self.put(user)
for role in roles: for role in roles:
self.add_role_to_user(user, role) self.add_role_to_user(user, role)
self.put(user)
return user return user
def add_role_to_user(self, user, role): def add_role_to_user(self, user, role):
"""Adds a role tp a user """Adds a role to a user.
:param user: The user to manipulate :param user: The user to manipulate
:param role: The role to add to the user :param role: The role to add to the user
@@ -309,11 +312,11 @@ class PeeweeUserDatastore(PeeweeDatastore, UserDatastore):
if result.count(): if result.count():
return False return False
else: else:
self.UserRole.create(user=user.id, role=role.id) self.put(self.UserRole.create(user=user.id, role=role.id))
return True return True
def remove_role_from_user(self, user, role): def remove_role_from_user(self, user, role):
"""Removes a role from a user """Removes a role from a user.
:param user: The user to manipulate :param user: The user to manipulate
:param role: The role to remove from the user :param role: The role to remove from the user
+37 -17
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.decorators flask_security.decorators
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security decorators module Flask-Security decorators module
@@ -13,8 +13,8 @@ from collections import namedtuple
from functools import wraps from functools import wraps
from flask import current_app, Response, request, redirect, _request_ctx_stack from flask import current_app, Response, request, redirect, _request_ctx_stack
from flask.ext.login import current_user, login_required from flask_login import current_user, login_required # pragma: no flakes
from flask.ext.principal import RoleNeed, Permission, Identity, identity_changed from flask_principal import RoleNeed, Permission, Identity, identity_changed
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from . import utils from . import utils
@@ -52,11 +52,12 @@ def _check_token():
header_token = request.headers.get(header_key, None) header_token = request.headers.get(header_key, None)
token = request.args.get(args_key, header_token) token = request.args.get(args_key, header_token)
if request.get_json(silent=True): if request.get_json(silent=True):
token = request.json.get(args_key, token) if not isinstance(request.json, list):
token = request.json.get(args_key, token)
user = _security.login_manager.token_callback(token) user = _security.login_manager.token_callback(token)
if user and user.is_authenticated(): if user and user.is_authenticated:
app = current_app._get_current_object() app = current_app._get_current_object()
_request_ctx_stack.top.user = user _request_ctx_stack.top.user = user
identity_changed.send(app, identity=Identity(user.id)) identity_changed.send(app, identity=Identity(user.id))
@@ -90,9 +91,12 @@ def http_auth_required(realm):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if _check_http_auth(): if _check_http_auth():
return fn(*args, **kwargs) return fn(*args, **kwargs)
r = _security.default_http_auth_realm if callable(realm) else realm if _security._unauthorized_callback:
h = {'WWW-Authenticate': 'Basic realm="%s"' % r} return _security._unauthorized_callback()
return _get_unauthorized_response(headers=h) else:
r = _security.default_http_auth_realm if callable(realm) else realm
h = {'WWW-Authenticate': 'Basic realm="%s"' % r}
return _get_unauthorized_response(headers=h)
return wrapper return wrapper
if callable(realm): if callable(realm):
@@ -112,7 +116,10 @@ def auth_token_required(fn):
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
if _check_token(): if _check_token():
return fn(*args, **kwargs) return fn(*args, **kwargs)
return _get_unauthorized_response() if _security._unauthorized_callback:
return _security._unauthorized_callback()
else:
return _get_unauthorized_response()
return decorated return decorated
@@ -131,17 +138,24 @@ def auth_required(*auth_methods):
login_mechanisms = { login_mechanisms = {
'token': lambda: _check_token(), 'token': lambda: _check_token(),
'basic': lambda: _check_http_auth(), 'basic': lambda: _check_http_auth(),
'session': lambda: current_user.is_authenticated() 'session': lambda: current_user.is_authenticated
} }
def wrapper(fn): def wrapper(fn):
@wraps(fn) @wraps(fn)
def decorated_view(*args, **kwargs): def decorated_view(*args, **kwargs):
mechanisms = [login_mechanisms.get(method) for method in auth_methods] h = {}
for mechanism in mechanisms: mechanisms = [(method, login_mechanisms.get(method)) for method in auth_methods]
for method, mechanism in mechanisms:
if mechanism and mechanism(): if mechanism and mechanism():
return fn(*args, **kwargs) return fn(*args, **kwargs)
return _get_unauthorized_response() elif method == 'basic':
r = _security.default_http_auth_realm
h['WWW-Authenticate'] = 'Basic realm="%s"' % r
if _security._unauthorized_callback:
return _security._unauthorized_callback()
else:
return _get_unauthorized_response(headers=h)
return decorated_view return decorated_view
return wrapper return wrapper
@@ -166,7 +180,10 @@ def roles_required(*roles):
perms = [Permission(RoleNeed(role)) for role in roles] perms = [Permission(RoleNeed(role)) for role in roles]
for perm in perms: for perm in perms:
if not perm.can(): if not perm.can():
return _get_unauthorized_view() if _security._unauthorized_callback:
return _security._unauthorized_callback()
else:
return _get_unauthorized_view()
return fn(*args, **kwargs) return fn(*args, **kwargs)
return decorated_view return decorated_view
return wrapper return wrapper
@@ -192,7 +209,10 @@ def roles_accepted(*roles):
perm = Permission(*[RoleNeed(role) for role in roles]) perm = Permission(*[RoleNeed(role) for role in roles])
if perm.can(): if perm.can():
return fn(*args, **kwargs) return fn(*args, **kwargs)
return _get_unauthorized_view() if _security._unauthorized_callback:
return _security._unauthorized_callback()
else:
return _get_unauthorized_view()
return decorated_view return decorated_view
return wrapper return wrapper
@@ -200,7 +220,7 @@ def roles_accepted(*roles):
def anonymous_user_required(f): def anonymous_user_required(f):
@wraps(f) @wraps(f)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if current_user.is_authenticated(): if current_user.is_authenticated:
return redirect(utils.get_url(_security.post_login_view)) return redirect(utils.get_url(_security.post_login_view))
return f(*args, **kwargs) return f(*args, **kwargs)
return wrapper return wrapper
+33 -29
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.forms flask_security.forms
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
Flask-Security forms module Flask-Security forms module
@@ -10,20 +10,16 @@
""" """
import inspect import inspect
try:
from urlparse import urlsplit
except ImportError:
from urllib.parse import urlsplit
from flask import request, current_app from flask import request, current_app, flash
from flask_wtf import Form as BaseForm from flask_wtf import Form as BaseForm
from wtforms import TextField, PasswordField, validators, \ from wtforms import StringField, PasswordField, validators, \
SubmitField, HiddenField, BooleanField, ValidationError, Field SubmitField, HiddenField, BooleanField, ValidationError, Field
from flask_login import current_user from flask_login import current_user
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from .confirmable import requires_confirmation from .confirmable import requires_confirmation
from .utils import verify_and_update_password, get_message, config_value from .utils import verify_and_update_password, get_message, config_value, validate_redirect_url
# Convenient reference # Convenient reference
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore) _datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
@@ -79,13 +75,13 @@ def get_form_field_label(key):
def unique_user_email(form, field): def unique_user_email(form, field):
if _datastore.find_user(email=field.data) is not None: if _datastore.get_user(field.data) is not None:
msg = get_message('EMAIL_ALREADY_ASSOCIATED', email=field.data)[0] msg = get_message('EMAIL_ALREADY_ASSOCIATED', email=field.data)[0]
raise ValidationError(msg) raise ValidationError(msg)
def valid_user_email(form, field): def valid_user_email(form, field):
form.user = _datastore.find_user(email=field.data) form.user = _datastore.get_user(field.data)
if form.user is None: if form.user is None:
raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0]) raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0])
@@ -98,20 +94,20 @@ class Form(BaseForm):
class EmailFormMixin(): class EmailFormMixin():
email = TextField( email = StringField(
get_form_field_label('email'), get_form_field_label('email'),
validators=[email_required, email_validator]) validators=[email_required, email_validator])
class UserEmailFormMixin(): class UserEmailFormMixin():
user = None user = None
email = TextField( email = StringField(
get_form_field_label('email'), get_form_field_label('email'),
validators=[email_required, email_validator, valid_user_email]) validators=[email_required, email_validator, valid_user_email])
class UniqueEmailFormMixin(): class UniqueEmailFormMixin():
email = TextField( email = StringField(
get_form_field_label('email'), get_form_field_label('email'),
validators=[email_required, email_validator, unique_user_email]) validators=[email_required, email_validator, unique_user_email])
@@ -137,12 +133,10 @@ class NextFormMixin():
next = HiddenField() next = HiddenField()
def validate_next(self, field): def validate_next(self, field):
if field.data: if field.data and not validate_redirect_url(field.data):
url_next = urlsplit(field.data) field.data = ''
url_base = urlsplit(request.host_url) flash(*get_message('INVALID_REDIRECT'))
if url_next.netloc and url_next.netloc != url_base.netloc: raise ValidationError(get_message('INVALID_REDIRECT')[0])
field.data = ''
raise ValidationError(get_message('INVALID_REDIRECT')[0])
class RegisterFormMixin(): class RegisterFormMixin():
@@ -181,6 +175,14 @@ class ForgotPasswordForm(Form, UserEmailFormMixin):
submit = SubmitField(get_form_field_label('recover_password')) submit = SubmitField(get_form_field_label('recover_password'))
def validate(self):
if not super(ForgotPasswordForm, self).validate():
return False
if requires_confirmation(self.user):
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
return False
return True
class PasswordlessLoginForm(Form, UserEmailFormMixin): class PasswordlessLoginForm(Form, UserEmailFormMixin):
"""The passwordless login form""" """The passwordless login form"""
@@ -193,7 +195,7 @@ class PasswordlessLoginForm(Form, UserEmailFormMixin):
def validate(self): def validate(self):
if not super(PasswordlessLoginForm, self).validate(): if not super(PasswordlessLoginForm, self).validate():
return False return False
if not self.user.is_active(): if not self.user.is_active:
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0]) self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
return False return False
return True return True
@@ -202,13 +204,15 @@ class PasswordlessLoginForm(Form, UserEmailFormMixin):
class LoginForm(Form, NextFormMixin): class LoginForm(Form, NextFormMixin):
"""The default login form""" """The default login form"""
email = TextField(get_form_field_label('email')) email = StringField(get_form_field_label('email'))
password = PasswordField(get_form_field_label('password')) password = PasswordField(get_form_field_label('password'))
remember = BooleanField(get_form_field_label('remember_me')) remember = BooleanField(get_form_field_label('remember_me'))
submit = SubmitField(get_form_field_label('login')) submit = SubmitField(get_form_field_label('login'))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs) super(LoginForm, self).__init__(*args, **kwargs)
if not self.next.data:
self.next.data = request.args.get('next', '')
self.remember.default = config_value('DEFAULT_REMEMBER_ME') self.remember.default = config_value('DEFAULT_REMEMBER_ME')
def validate(self): def validate(self):
@@ -223,7 +227,6 @@ class LoginForm(Form, NextFormMixin):
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0]) self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
return False return False
self.user = _datastore.get_user(self.email.data) self.user = _datastore.get_user(self.email.data)
if self.user is None: if self.user is None:
@@ -238,7 +241,7 @@ class LoginForm(Form, NextFormMixin):
if requires_confirmation(self.user): if requires_confirmation(self.user):
self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0]) self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0])
return False return False
if not self.user.is_active(): if not self.user.is_active:
self.email.errors.append(get_message('DISABLED_ACCOUNT')[0]) self.email.errors.append(get_message('DISABLED_ACCOUNT')[0])
return False return False
return True return True
@@ -249,8 +252,12 @@ class ConfirmRegisterForm(Form, RegisterFormMixin,
pass pass
class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin): class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin,
pass NextFormMixin):
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
if not self.next.data:
self.next.data = request.args.get('next', '')
class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin): class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin):
@@ -276,9 +283,6 @@ class ChangePasswordForm(Form, PasswordFormMixin):
if not super(ChangePasswordForm, self).validate(): if not super(ChangePasswordForm, self).validate():
return False return False
if self.password.data.strip() == '':
self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0])
return False
if not verify_and_update_password(self.password.data, current_user): if not verify_and_update_password(self.password.data, current_user):
self.password.errors.append(get_message('INVALID_PASSWORD')[0]) self.password.errors.append(get_message('INVALID_PASSWORD')[0])
return False return False
+3 -4
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.passwordless flask_security.passwordless
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security passwordless module Flask-Security passwordless module
@@ -35,8 +35,7 @@ def send_login_instructions(user):
send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email, send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email,
'login_instructions', user=user, login_link=login_link) 'login_instructions', user=user, login_link=login_link)
login_instructions_sent.send(app._get_current_object(), login_instructions_sent.send(app._get_current_object(), user=user, login_token=token)
user=user, login_token=token)
def generate_login_token(user): def generate_login_token(user):
+16 -7
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.recoverable flask_security.recoverable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security recoverable module Flask-Security recoverable module
@@ -11,6 +11,7 @@
from flask import current_app as app from flask import current_app as app
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from werkzeug.security import safe_str_cmp
from .signals import password_reset, reset_password_instructions_sent from .signals import password_reset, reset_password_instructions_sent
from .utils import send_mail, md5, encrypt_password, url_for_security, \ from .utils import send_mail, md5, encrypt_password, url_for_security, \
@@ -35,8 +36,7 @@ def send_reset_password_instructions(user):
'reset_instructions', 'reset_instructions',
user=user, reset_link=reset_link) user=user, reset_link=reset_link)
reset_password_instructions_sent.send(app._get_current_object(), reset_password_instructions_sent.send(app._get_current_object(), user=user, token=token)
user=user, token=token)
def send_password_reset_notice(user): def send_password_reset_notice(user):
@@ -54,7 +54,8 @@ def generate_reset_password_token(user):
:param user: The user to work with :param user: The user to work with
""" """
data = [str(user.id), md5(user.password)] password_hash = md5(user.password) if user.password else None
data = [str(user.id), password_hash]
return _security.reset_serializer.dumps(data) return _security.reset_serializer.dumps(data)
@@ -62,11 +63,19 @@ def reset_password_token_status(token):
"""Returns the expired status, invalid status, and user of a password reset """Returns the expired status, invalid status, and user of a password reset
token. For example:: token. For example::
expired, invalid, user = reset_password_token_status('...') expired, invalid, user, data = reset_password_token_status('...')
:param token: The password reset token :param token: The password reset token
""" """
return get_token_status(token, 'reset', 'RESET_PASSWORD') expired, invalid, user, data = get_token_status(token, 'reset', 'RESET_PASSWORD',
return_data=True)
if not invalid:
if user.password:
password_hash = md5(user.password)
if not safe_str_cmp(password_hash, data[1]):
invalid = True
return expired, invalid, user
def update_password(user, password): def update_password(user, password):
+2 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.registerable flask_security.registerable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security registerable module Flask-Security registerable module
+18 -16
View File
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.script flask_security.script
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
Flask-Security script module Flask-Security script module
:copyright: (c) 2012 by Matt Wright. :copyright: (c) 2012 by Matt Wright.
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
from __future__ import print_function
try: try:
import simplejson as json import simplejson as json
except ImportError: except ImportError:
@@ -16,7 +18,7 @@ except ImportError:
import re import re
from flask import current_app from flask import current_app
from flask.ext.script import Command, Option from flask_script import Command, Option
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from .utils import encrypt_password from .utils import encrypt_password
@@ -26,7 +28,7 @@ _datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
def pprint(obj): def pprint(obj):
print json.dumps(obj, sort_keys=True, indent=4) print(json.dumps(obj, sort_keys=True, indent=4))
def commit(fn): def commit(fn):
@@ -40,9 +42,9 @@ class CreateUserCommand(Command):
"""Create a user""" """Create a user"""
option_list = ( option_list = (
Option('-e', '--email', dest='email', default=None), Option('-e', '--email', dest='email', default=None),
Option('-p', '--password', dest='password', default=None), Option('-p', '--password', dest='password', default=None),
Option('-a', '--active', dest='active', default=''), Option('-a', '--active', dest='active', default=''),
) )
@commit @commit
@@ -59,11 +61,11 @@ class CreateUserCommand(Command):
if form.validate(): if form.validate():
kwargs['password'] = encrypt_password(kwargs['password']) kwargs['password'] = encrypt_password(kwargs['password'])
_datastore.create_user(**kwargs) _datastore.create_user(**kwargs)
print 'User created successfully.' print('User created successfully.')
kwargs['password'] = '****' kwargs['password'] = '****'
pprint(kwargs) pprint(kwargs)
else: else:
print 'Error creating user' print('Error creating user')
pprint(form.errors) pprint(form.errors)
@@ -78,7 +80,7 @@ class CreateRoleCommand(Command):
@commit @commit
def run(self, **kwargs): def run(self, **kwargs):
_datastore.create_role(**kwargs) _datastore.create_role(**kwargs)
print 'Role "%(name)s" created successfully.' % kwargs print('Role "%(name)s" created successfully.' % kwargs)
class _RoleCommand(Command): class _RoleCommand(Command):
@@ -94,16 +96,16 @@ class AddRoleCommand(_RoleCommand):
@commit @commit
def run(self, user_identifier, role_name): def run(self, user_identifier, role_name):
_datastore.add_role_to_user(user_identifier, role_name) _datastore.add_role_to_user(user_identifier, role_name)
print "Role '%s' added to user '%s' successfully" % (role_name, user_identifier) print("Role '%s' added to user '%s' successfully" % (role_name, user_identifier))
class RemoveRoleCommand(_RoleCommand): class RemoveRoleCommand(_RoleCommand):
"""Add a role to a user""" """Remove a role from a user"""
@commit @commit
def run(self, user_identifier, role_name): def run(self, user_identifier, role_name):
_datastore.remove_role_from_user(user_identifier, role_name) _datastore.remove_role_from_user(user_identifier, role_name)
print "Role '%s' removed from user '%s' successfully" % (role_name, user_identifier) print("Role '%s' removed from user '%s' successfully" % (role_name, user_identifier))
class _ToggleActiveCommand(Command): class _ToggleActiveCommand(Command):
@@ -113,18 +115,18 @@ class _ToggleActiveCommand(Command):
class DeactivateUserCommand(_ToggleActiveCommand): class DeactivateUserCommand(_ToggleActiveCommand):
"""Deactive a user""" """Deactivate a user"""
@commit @commit
def run(self, user_identifier): def run(self, user_identifier):
_datastore.deactivate_user(user_identifier) _datastore.deactivate_user(user_identifier)
print "User '%s' has been deactivated" % user_identifier print("User '%s' has been deactivated" % user_identifier)
class ActivateUserCommand(_ToggleActiveCommand): class ActivateUserCommand(_ToggleActiveCommand):
"""Deactive a user""" """Activate a user"""
@commit @commit
def run(self, user_identifier): def run(self, user_identifier):
_datastore.activate_user(user_identifier) _datastore.activate_user(user_identifier)
print "User '%s' has been activated" % user_identifier print("User '%s' has been activated" % user_identifier)
+2 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.signals flask_security.signals
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
Flask-Security signals module Flask-Security signals module
+3 -3
View File
@@ -1,9 +1,9 @@
{% if security.registerable or security.recoverable or security.confirmabled %} {% if security.registerable or security.recoverable or security.confirmable %}
<h2>Menu</h2> <h2>Menu</h2>
<ul> <ul>
<li><a href="{{ url_for_security('login') }}">Login</a></li> <li><a href="{{ url_for_security('login') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">Login</a></li>
{% if security.registerable %} {% if security.registerable %}
<li><a href="{{ url_for_security('register') }}">Register</a><br/></li> <li><a href="{{ url_for_security('register') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">Register</a><br/></li>
{% endif %} {% endif %}
{% if security.recoverable %} {% if security.recoverable %}
<li><a href="{{ url_for_security('forgot_password') }}">Forgot password</a><br/></li> <li><a href="{{ url_for_security('forgot_password') }}">Forgot password</a><br/></li>
+91 -93
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.utils flask_security.utils
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
Flask-Security utils module Flask-Security utils module
@@ -10,26 +10,26 @@
""" """
import base64 import base64
import blinker
import functools
import hashlib import hashlib
import hmac import hmac
import sys import sys
try:
from urlparse import urlsplit
except ImportError: # pragma: no cover
from urllib.parse import urlsplit
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime, timedelta from datetime import datetime, timedelta
from flask import url_for, flash, current_app, request, session, render_template from flask import url_for, flash, current_app, request, session, render_template
from flask.ext.login import login_user as _login_user, \ from flask_login import login_user as _login_user, logout_user as _logout_user
logout_user as _logout_user from flask_mail import Message
from flask.ext.mail import Message from flask_principal import Identity, AnonymousIdentity, identity_changed
from flask.ext.principal import Identity, AnonymousIdentity, identity_changed
from itsdangerous import BadSignature, SignatureExpired from itsdangerous import BadSignature, SignatureExpired
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from .signals import user_registered, user_confirmed, \ from .signals import user_registered, login_instructions_sent, reset_password_instructions_sent
confirm_instructions_sent, login_instructions_sent, \
password_reset, password_changed, reset_password_instructions_sent
# Convenient references # Convenient references
_security = LocalProxy(lambda: current_app.extensions['security']) _security = LocalProxy(lambda: current_app.extensions['security'])
@@ -40,12 +40,12 @@ _pwd_context = LocalProxy(lambda: _security.pwd_context)
PY3 = sys.version_info[0] == 3 PY3 = sys.version_info[0] == 3
if PY3: if PY3: # pragma: no cover
string_types = str, string_types = str, # pragma: no flakes
text_type = str text_type = str # pragma: no flakes
else: else: # pragma: no cover
string_types = basestring, string_types = basestring, # pragma: no flakes
text_type = unicode text_type = unicode # pragma: no flakes
def login_user(user, remember=None): def login_user(user, remember=None):
@@ -58,12 +58,16 @@ def login_user(user, remember=None):
if remember is None: if remember is None:
remember = config_value('DEFAULT_REMEMBER_ME') remember = config_value('DEFAULT_REMEMBER_ME')
if not _login_user(user, remember): if not _login_user(user, remember): # pragma: no cover
return False return False
if _security.trackable: if _security.trackable:
if 'X-Forwarded-For' in request.headers:
remote_addr = request.headers.getlist("X-Forwarded-For")[0].rpartition(' ')[-1]
else:
remote_addr = request.remote_addr or 'untrackable'
old_current_login, new_current_login = user.current_login_at, datetime.utcnow() old_current_login, new_current_login = user.current_login_at, datetime.utcnow()
remote_addr = request.remote_addr or 'untrackable'
old_current_ip, new_current_ip = user.current_login_ip, remote_addr old_current_ip, new_current_ip = user.current_login_ip, remote_addr
user.last_login_at = old_current_login or new_current_login user.last_login_at = old_current_login or new_current_login
@@ -95,13 +99,15 @@ def get_hmac(password):
:param password: The password to sign :param password: The password to sign
""" """
if _security.password_salt is None: salt = _security.password_salt
if salt is None:
raise RuntimeError( raise RuntimeError(
'The configuration value `SECURITY_PASSWORD_SALT` must ' 'The configuration value `SECURITY_PASSWORD_SALT` must '
'not be None when the value of `SECURITY_PASSWORD_HASH` is ' 'not be None when the value of `SECURITY_PASSWORD_HASH` is '
'set to "%s"' % _security.password_hash) 'set to "%s"' % _security.password_hash)
h = hmac.new(_security.password_salt.encode('utf-8'), password.encode('utf-8'), hashlib.sha512) h = hmac.new(encode_string(salt), encode_string(password), hashlib.sha512)
return base64.b64encode(h.digest()) return base64.b64encode(h.digest())
@@ -109,9 +115,12 @@ def verify_password(password, password_hash):
"""Returns ``True`` if the password matches the supplied hash. """Returns ``True`` if the password matches the supplied hash.
:param password: A plaintext password to verify :param password: A plaintext password to verify
:param password_hash: The expected hash value of the password (usually form your database) :param password_hash: The expected hash value of the password (usually from your database)
""" """
return _pwd_context.verify(encrypt_password(password), password_hash) if _security.password_hash != 'plaintext':
password = get_hmac(password)
return _pwd_context.verify(password, password_hash)
def verify_and_update_password(password, user): def verify_and_update_password(password, user):
@@ -121,9 +130,12 @@ def verify_and_update_password(password, user):
:param password: A plaintext password to verify :param password: A plaintext password to verify
:param user: The user to verify against :param user: The user to verify against
""" """
verified, new_password = _pwd_context.verify_and_update(encrypt_password(password), user.password)
if _pwd_context.identify(user.password) != 'plaintext':
password = get_hmac(password)
verified, new_password = _pwd_context.verify_and_update(password, user.password)
if verified and new_password: if verified and new_password:
user.password = new_password user.password = encrypt_password(password)
_datastore.put(user) _datastore.put(user)
return verified return verified
@@ -131,16 +143,26 @@ def verify_and_update_password(password, user):
def encrypt_password(password): def encrypt_password(password):
"""Encrypts the specified plaintext password using the configured encryption options. """Encrypts the specified plaintext password using the configured encryption options.
:param password: The plaintext passwrod to encrypt :param password: The plaintext password to encrypt
""" """
if _security.password_hash == 'plaintext': if _security.password_hash == 'plaintext':
return password return password
signed = get_hmac(password) signed = get_hmac(password).decode('ascii')
return _pwd_context.encrypt(signed.decode('ascii')) return _pwd_context.encrypt(signed)
def encode_string(string):
"""Encodes a string to bytes, if it isn't already.
:param string: The string to encode"""
if isinstance(string, text_type):
string = string.encode('utf-8')
return string
def md5(data): def md5(data):
return hashlib.md5(data.encode('ascii')).hexdigest() return hashlib.md5(encode_string(data)).hexdigest()
def do_flash(message, category=None): def do_flash(message, category=None):
@@ -166,6 +188,14 @@ def get_url(endpoint_or_url):
return endpoint_or_url return endpoint_or_url
def slash_url_suffix(url, suffix):
"""Adds a slash either to the beginning or the end of a suffix
(which is to be appended to a URL), depending on whether or not
the URL ends with a slash."""
return url.endswith('/') and ('%s/' % suffix) or ('/%s' % suffix)
def get_security_endpoint_name(endpoint): def get_security_endpoint_name(endpoint):
return '%s.%s' % (_security.blueprint_name, endpoint) return '%s.%s' % (_security.blueprint_name, endpoint)
@@ -185,18 +215,35 @@ def url_for_security(endpoint, **values):
return url_for(endpoint, **values) return url_for(endpoint, **values)
def get_post_action_redirect(config_key): def validate_redirect_url(url):
return (get_url(request.args.get('next')) or if url is None or url.strip() == '':
get_url(request.form.get('next')) or return False
find_redirect(config_key)) url_next = urlsplit(url)
url_base = urlsplit(request.host_url)
if (url_next.netloc or url_next.scheme) and url_next.netloc != url_base.netloc:
return False
return True
def get_post_login_redirect(): def get_post_action_redirect(config_key, declared=None):
return get_post_action_redirect('SECURITY_POST_LOGIN_VIEW') urls = [
get_url(request.args.get('next')),
get_url(request.form.get('next')),
find_redirect(config_key)
]
if declared:
urls.insert(0, declared)
for url in urls:
if validate_redirect_url(url):
return url
def get_post_register_redirect(): def get_post_login_redirect(declared=None):
return get_post_action_redirect('SECURITY_POST_REGISTER_VIEW') return get_post_action_redirect('SECURITY_POST_LOGIN_VIEW', declared)
def get_post_register_redirect(declared=None):
return get_post_action_redirect('SECURITY_POST_REGISTER_VIEW', declared)
def find_redirect(key): def find_redirect(key):
@@ -294,7 +341,7 @@ def send_mail(subject, recipient, template, **context):
mail.send(msg) mail.send(msg)
def get_token_status(token, serializer, max_age=None): def get_token_status(token, serializer, max_age=None, return_data=False):
"""Get the status of a token. """Get the status of a token.
:param token: The token to check :param token: The token to check
@@ -313,14 +360,18 @@ def get_token_status(token, serializer, max_age=None):
except SignatureExpired: except SignatureExpired:
d, data = serializer.loads_unsafe(token) d, data = serializer.loads_unsafe(token)
expired = True expired = True
except BadSignature: except (BadSignature, TypeError, ValueError):
invalid = True invalid = True
if data: if data:
user = _datastore.find_user(id=data[0]) user = _datastore.find_user(id=data[0])
expired = expired and (user is not None) expired = expired and (user is not None)
return expired, invalid, user
if return_data:
return expired, invalid, user, data
else:
return expired, invalid, user
def get_identity_attributes(app=None): def get_identity_attributes(app=None):
@@ -386,56 +437,3 @@ def capture_reset_password_requests(reset_password_sent_at=None):
yield reset_requests yield reset_requests
finally: finally:
reset_password_instructions_sent.disconnect(_on) reset_password_instructions_sent.disconnect(_on)
class CaptureSignals(object):
"""Testing utility for capturing blinker signals.
Context manager which mocks out selected signals and registers which are `sent` on and what
arguments were sent. Instantiate with a list of blinker `NamedSignals` to patch. Each signal
has it's `send` mocked out.
"""
def __init__(self, signals):
"""Patch all given signals and make them available as attributes.
:param signals: list of signals
"""
self._records = {}
self._receivers = {}
for signal in signals:
self._records[signal] = []
self._receivers[signal] = functools.partial(self._record, signal)
def __getitem__(self, signal):
"""All captured signals are available via `ctxt[signal]`.
"""
if isinstance(signal, blinker.base.NamedSignal):
return self._records[signal]
else:
super(CaptureSignals, self).__setitem__(signal)
def _record(self, signal, *args, **kwargs):
self._records[signal].append((args, kwargs))
def __enter__(self):
for signal, receiver in self._receivers.items():
signal.connect(receiver)
return self
def __exit__(self, type, value, traceback):
for signal, receiver in self._receivers.items():
signal.disconnect(receiver)
def signals_sent(self):
"""Return a set of the signals sent.
:rtype: list of blinker `NamedSignals`.
"""
return set([signal for signal, _ in self._records.items() if self._records[signal]])
def capture_signals():
"""Factory method that creates a `CaptureSignals` with all the flask_security signals."""
return CaptureSignals([user_registered, user_confirmed,
confirm_instructions_sent, login_instructions_sent,
password_reset, password_changed,
reset_password_instructions_sent])
+54 -44
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
flask.ext.security.views flask_security.views
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
Flask-Security views module Flask-Security views module
@@ -9,7 +9,7 @@
:license: MIT, see LICENSE for more details. :license: MIT, see LICENSE for more details.
""" """
from flask import current_app, redirect, request, render_template, jsonify, \ from flask import current_app, redirect, request, jsonify, \
after_this_request, Blueprint after_this_request, Blueprint
from flask_login import current_user from flask_login import current_user
from werkzeug.datastructures import MultiDict from werkzeug.datastructures import MultiDict
@@ -26,7 +26,7 @@ from .changeable import change_user_password
from .registerable import register_user from .registerable import register_user
from .utils import config_value, do_flash, get_url, get_post_login_redirect, \ from .utils import config_value, do_flash, get_url, get_post_login_redirect, \
get_post_register_redirect, get_message, login_user, logout_user, \ get_post_register_redirect, get_message, login_user, logout_user, \
url_for_security as url_for url_for_security as url_for, slash_url_suffix
# Convenient references # Convenient references
_security = LocalProxy(lambda: current_app.extensions['security']) _security = LocalProxy(lambda: current_app.extensions['security'])
@@ -34,7 +34,7 @@ _security = LocalProxy(lambda: current_app.extensions['security'])
_datastore = LocalProxy(lambda: _security.datastore) _datastore = LocalProxy(lambda: _security.datastore)
def _render_json(form, include_auth_token=False): def _render_json(form, include_user=True, include_auth_token=False):
has_errors = len(form.errors) > 0 has_errors = len(form.errors) > 0
if has_errors: if has_errors:
@@ -42,7 +42,9 @@ def _render_json(form, include_auth_token=False):
response = dict(errors=form.errors) response = dict(errors=form.errors)
else: else:
code = 200 code = 200
response = dict(user=dict(id=str(form.user.id))) response = dict()
if include_user:
response['user'] = dict(id=str(form.user.id))
if include_auth_token: if include_auth_token:
token = form.user.get_auth_token() token = form.user.get_auth_token()
response['user']['authentication_token'] = token response['user']['authentication_token'] = token
@@ -75,29 +77,27 @@ def login():
after_this_request(_commit) after_this_request(_commit)
if not request.json: if not request.json:
return redirect(get_post_login_redirect()) return redirect(get_post_login_redirect(form.next.data))
form.next.data = get_url(request.args.get('next')) \
or get_url(request.form.get('next')) or ''
if request.json: if request.json:
return _render_json(form, True) return _render_json(form, include_auth_token=True)
return render_template(config_value('LOGIN_USER_TEMPLATE'), return _security.render_template(config_value('LOGIN_USER_TEMPLATE'),
login_user_form=form, login_user_form=form,
**_ctx('login')) **_ctx('login'))
@login_required
def logout(): def logout():
"""View function which handles a logout request.""" """View function which handles a logout request."""
logout_user() if current_user.is_authenticated:
logout_user()
return redirect(request.args.get('next', None) or return redirect(request.args.get('next', None) or
get_url(_security.post_logout_view)) get_url(_security.post_logout_view))
@anonymous_user_required
def register(): def register():
"""View function which handles a registration request.""" """View function which handles a registration request."""
@@ -122,15 +122,20 @@ def register():
login_user(user) login_user(user)
if not request.json: if not request.json:
return redirect(get_post_register_redirect()) if 'next' in form:
return _render_json(form, True) redirect_url = get_post_register_redirect(form.next.data)
else:
redirect_url = get_post_register_redirect()
return redirect(redirect_url)
return _render_json(form, include_auth_token=True)
if request.json: if request.json:
return _render_json(form) return _render_json(form)
return render_template(config_value('REGISTER_USER_TEMPLATE'), return _security.render_template(config_value('REGISTER_USER_TEMPLATE'),
register_user_form=form, register_user_form=form,
**_ctx('register')) **_ctx('register'))
def send_login(): def send_login():
@@ -151,9 +156,9 @@ def send_login():
if request.json: if request.json:
return _render_json(form) return _render_json(form)
return render_template(config_value('SEND_LOGIN_TEMPLATE'), return _security.render_template(config_value('SEND_LOGIN_TEMPLATE'),
send_login_form=form, send_login_form=form,
**_ctx('send_login')) **_ctx('send_login'))
@anonymous_user_required @anonymous_user_required
@@ -196,9 +201,9 @@ def send_confirmation():
if request.json: if request.json:
return _render_json(form) return _render_json(form)
return render_template(config_value('SEND_CONFIRMATION_TEMPLATE'), return _security.render_template(config_value('SEND_CONFIRMATION_TEMPLATE'),
send_confirmation_form=form, send_confirmation_form=form,
**_ctx('send_confirmation')) **_ctx('send_confirmation'))
def confirm_email(token): def confirm_email(token):
@@ -221,14 +226,19 @@ def confirm_email(token):
logout_user() logout_user()
login_user(user) login_user(user)
confirm_user(user) if confirm_user(user):
after_this_request(_commit) after_this_request(_commit)
do_flash(*get_message('EMAIL_CONFIRMED')) msg = 'EMAIL_CONFIRMED'
else:
msg = 'ALREADY_CONFIRMED'
do_flash(*get_message(msg))
return redirect(get_url(_security.post_confirm_view) or return redirect(get_url(_security.post_confirm_view) or
get_url(_security.post_login_view)) get_url(_security.post_login_view))
@anonymous_user_required
def forgot_password(): def forgot_password():
"""View function that handles a forgotten password request.""" """View function that handles a forgotten password request."""
@@ -245,11 +255,11 @@ def forgot_password():
do_flash(*get_message('PASSWORD_RESET_REQUEST', email=form.user.email)) do_flash(*get_message('PASSWORD_RESET_REQUEST', email=form.user.email))
if request.json: if request.json:
return _render_json(form) return _render_json(form, include_user=False)
return render_template(config_value('FORGOT_PASSWORD_TEMPLATE'), return _security.render_template(config_value('FORGOT_PASSWORD_TEMPLATE'),
forgot_password_form=form, forgot_password_form=form,
**_ctx('forgot_password')) **_ctx('forgot_password'))
@anonymous_user_required @anonymous_user_required
@@ -276,10 +286,10 @@ def reset_password(token):
return redirect(get_url(_security.post_reset_view) or return redirect(get_url(_security.post_reset_view) or
get_url(_security.post_login_view)) get_url(_security.post_login_view))
return render_template(config_value('RESET_PASSWORD_TEMPLATE'), return _security.render_template(config_value('RESET_PASSWORD_TEMPLATE'),
reset_password_form=form, reset_password_form=form,
reset_password_token=token, reset_password_token=token,
**_ctx('reset_password')) **_ctx('reset_password'))
@login_required @login_required
@@ -305,9 +315,9 @@ def change_password():
form.user = current_user form.user = current_user
return _render_json(form) return _render_json(form)
return render_template(config_value('CHANGE_PASSWORD_TEMPLATE'), return _security.render_template(config_value('CHANGE_PASSWORD_TEMPLATE'),
change_password_form=form, change_password_form=form,
**_ctx('change_password')) **_ctx('change_password'))
def create_blueprint(state, import_name): def create_blueprint(state, import_name):
@@ -324,7 +334,7 @@ def create_blueprint(state, import_name):
bp.route(state.login_url, bp.route(state.login_url,
methods=['GET', 'POST'], methods=['GET', 'POST'],
endpoint='login')(send_login) endpoint='login')(send_login)
bp.route(state.login_url + '/<token>', bp.route(state.login_url + slash_url_suffix(state.login_url, '<token>'),
endpoint='token_login')(token_login) endpoint='token_login')(token_login)
else: else:
bp.route(state.login_url, bp.route(state.login_url,
@@ -340,7 +350,7 @@ def create_blueprint(state, import_name):
bp.route(state.reset_url, bp.route(state.reset_url,
methods=['GET', 'POST'], methods=['GET', 'POST'],
endpoint='forgot_password')(forgot_password) endpoint='forgot_password')(forgot_password)
bp.route(state.reset_url + '/<token>', bp.route(state.reset_url + slash_url_suffix(state.reset_url, '<token>'),
methods=['GET', 'POST'], methods=['GET', 'POST'],
endpoint='reset_password')(reset_password) endpoint='reset_password')(reset_password)
@@ -353,7 +363,7 @@ def create_blueprint(state, import_name):
bp.route(state.confirm_url, bp.route(state.confirm_url,
methods=['GET', 'POST'], methods=['GET', 'POST'],
endpoint='send_confirmation')(send_confirmation) endpoint='send_confirmation')(send_confirmation)
bp.route(state.confirm_url + '/<token>', bp.route(state.confirm_url + slash_url_suffix(state.confirm_url, '<token>'),
methods=['GET', 'POST'], methods=['GET', 'POST'],
endpoint='confirm_email')(confirm_email) endpoint='confirm_email')(confirm_email)
+10
View File
@@ -0,0 +1,10 @@
Flask-SQLAlchemy>=1.0
bcrypt>=1.0.2,<2.0.0
flask-mongoengine>=0.7.0,<0.7.3
flask-peewee>=0.6.5
pymongo==2.8
pytest>=2.5.2
pytest-cov>=1.6
pytest-flakes>=0.2
pytest-pep8>=1.0.5
tox>=1.7.0
+1 -1
View File
@@ -1,5 +1,5 @@
Flask>=0.9 Flask>=0.9
Flask-Login>=0.1.3 Flask-Login>=0.3.0,<0.4
Flask-Mail>=0.7.3 Flask-Mail>=0.7.3
Flask-Principal>=0.3.3 Flask-Principal>=0.3.3
Flask-WTF>=0.8 Flask-WTF>=0.8
+14 -1
View File
@@ -3,4 +3,17 @@ source-dir = docs/
build-dir = docs/_build build-dir = docs/_build
[upload_sphinx] [upload_sphinx]
upload-dir = docs/_build/html upload-dir = docs/_build/html
[pytest]
pep8maxlinelength = 99
pep8ignore =
docs/* ALL
scripts/* ALL
flakes-ignore =
ImportStarUsed
flask_security/__init__.py UnusedImport
docs/* ALL
scripts/* ALL
+44 -36
View File
@@ -1,55 +1,63 @@
""" """
Flask-Security Flask-Security
============== ==============
Flask-Security is a Flask extension that aims to add quick and simple security
to your Flask applications.
Resources
---------
* `Documentation <http://packages.python.org/Flask-Security/>`_
* `Issue Tracker <https://github.com/mattupstate/flask-security/issues>`_
* `Source <https://github.com/mattupstate/flask-security>`_
* `Development Version
<https://github.com/mattupstate/flask-security/raw/develop#egg=Flask-Security-dev>`_
""" """
from setuptools import setup import multiprocessing # pragma: no flakes
import sys
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
def get_requirements(suffix=''):
with open('requirements%s.txt' % suffix) as f:
rv = f.read().splitlines()
return rv
def get_long_description():
with open('README.rst') as f:
rv = f.read()
return rv
class PyTest(TestCommand):
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = [
'-xrs',
'--cov', 'flask_security',
'--cov-report', 'term-missing',
'--pep8',
'--flakes',
'--cache-clear'
]
self.test_suite = True
def run_tests(self):
import pytest
errno = pytest.main(self.test_args)
sys.exit(errno)
setup( setup(
name='Flask-Security', name='Flask-Security',
version='1.7.0', version='1.7.5',
url='https://github.com/mattupstate/flask-security', url='https://github.com/mattupstate/flask-security',
license='MIT', license='MIT',
author='Matt Wright', author='Matt Wright',
author_email='matt@nobien.net', author_email='matt@nobien.net',
description='Simple security for Flask apps', description='Simple security for Flask apps',
long_description=__doc__, long_description=get_long_description(),
packages=[ packages=find_packages(),
'flask_security'
],
zip_safe=False, zip_safe=False,
include_package_data=True, include_package_data=True,
platforms='any', platforms='any',
install_requires=[ install_requires=get_requirements(),
'Flask>=0.10.1', tests_require=get_requirements('-dev'),
'Flask-Login>=0.2.9', cmdclass={'test': PyTest},
'Flask-Mail>=0.9.0',
'Flask-Principal>=0.4.0',
'Flask-WTF>=0.9.3',
'passlib>=1.6.2',
],
test_suite='nose.collector',
tests_require=[
'nose',
'Flask-SQLAlchemy',
'Flask-MongoEngine',
'Flask-Peewee',
'bcrypt',
'simplejson'
],
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
'Environment :: Web Environment', 'Environment :: Web Environment',
-85
View File
@@ -1,85 +0,0 @@
# -*- coding: utf-8 -*-
import hmac
from hashlib import sha1
from unittest import TestCase
from tests.test_app.sqlalchemy import create_app
class SecurityTest(TestCase):
APP_KWARGS = {
'register_blueprint': True,
}
AUTH_CONFIG = None
def setUp(self):
super(SecurityTest, self).setUp()
app_kwargs = self.APP_KWARGS
app = self._create_app(self.AUTH_CONFIG or {}, **app_kwargs)
app.debug = False
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
self.app = app
self.client = app.test_client()
def _create_app(self, auth_config, **kwargs):
return create_app(auth_config, **kwargs)
def _get(self, route, content_type=None, follow_redirects=None, headers=None):
return self.client.get(route, follow_redirects=follow_redirects,
content_type=content_type or 'text/html',
headers=headers)
def _post(self, route, data=None, content_type=None, follow_redirects=True, headers=None):
content_type = content_type or 'application/x-www-form-urlencoded'
return self.client.post(route, data=data,
follow_redirects=follow_redirects,
content_type=content_type, headers=headers)
def register(self, email, password='password'):
data = dict(email=email, password=password)
return self.client.post('/register', data=data, follow_redirects=True)
def authenticate(self, email="matt@lp.com", password="password", endpoint=None, **kwargs):
data = dict(email=email, password=password, remember='y')
return self._post(endpoint or '/login', data=data, **kwargs)
def json_authenticate(self, email="matt@lp.com", password="password", endpoint=None):
data = """{
"email": "%s",
"password": "%s"
}"""
return self._post(endpoint or '/login', content_type="application/json",
data=data % (email, password))
def logout(self, endpoint=None):
return self._get(endpoint or '/logout', follow_redirects=True)
def assertIsHomePage(self, data):
self.assertIn(b'Home Page', data)
def assertIn(self, member, container, msg=None):
if hasattr(TestCase, 'assertIn'):
return TestCase.assertIn(self, member, container, msg)
return self.assertTrue(member in container)
def assertNotIn(self, member, container, msg=None):
if hasattr(TestCase, 'assertNotIn'):
return TestCase.assertNotIn(self, member, container, msg)
return self.assertFalse(member in container)
def assertIsNotNone(self, obj, msg=None):
if hasattr(TestCase, 'assertIsNotNone'):
return TestCase.assertIsNotNone(self, obj, msg)
return self.assertTrue(obj is not None)
def get_message(self, key, **kwargs):
return self.app.config['SECURITY_MSG_' + key][0] % kwargs
-854
View File
@@ -1,854 +0,0 @@
# -*- coding: utf-8 -*-
# from __future__ import with_statement
import base64
import time
import simplejson as json
import flask
from flask_security.utils import capture_registrations, \
capture_reset_password_requests, capture_passwordless_login_requests
from flask_security.forms import LoginForm, ConfirmRegisterForm, RegisterForm, \
ForgotPasswordForm, ResetPasswordForm, SendConfirmationForm, \
PasswordlessLoginForm
from flask_security.forms import TextField, SubmitField, valid_user_email
from flask_security.signals import user_registered
from tests import SecurityTest
# TODO: Wait for passlib + bcrypt python3 compatibility to be fixed
# class ConfiguredPasswordHashSecurityTests(SecurityTest):
# AUTH_CONFIG = {
# 'SECURITY_PASSWORD_HASH': 'bcrypt',
# 'SECURITY_PASSWORD_SALT': 'so-salty',
# 'USER_COUNT': 1
# }
# def test_authenticate(self):
# r = self.authenticate(endpoint="/login")
# self.assertIn(b'Home Page', r.data)
class ConfiguredSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True,
'SECURITY_LOGOUT_URL': '/custom_logout',
'SECURITY_LOGIN_URL': '/custom_login',
'SECURITY_POST_LOGIN_VIEW': '/post_login',
'SECURITY_POST_LOGOUT_VIEW': '/post_logout',
'SECURITY_POST_REGISTER_VIEW': '/post_register',
'SECURITY_UNAUTHORIZED_VIEW': '/unauthorized',
'SECURITY_DEFAULT_HTTP_AUTH_REALM': 'Custom Realm'
}
def test_login_view(self):
r = self._get('/custom_login')
self.assertIn(b"<h1>Login</h1>", r.data)
def test_authenticate(self):
r = self.authenticate(endpoint="/custom_login")
self.assertIn(b'Post Login', r.data)
def test_logout(self):
self.authenticate(endpoint="/custom_login")
r = self.logout(endpoint="/custom_logout")
self.assertIn(b'Post Logout', r.data)
def test_register_view(self):
r = self._get('/register')
self.assertIn(b'<h1>Register</h1>', r.data)
def test_register(self):
data = dict(email='dude@lp.com',
password='password',
password_confirm='password')
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn(b'Post Register', r.data)
def test_register_with_next_querystring_argument(self):
data = dict(email='dude@lp.com',
password='password',
password_confirm='password')
r = self._post('/register?next=/page1', data=data, follow_redirects=True)
self.assertIn(b'Page 1', r.data)
def test_register_json(self):
data = '{ "email": "dude@lp.com", "password": "password"}'
r = self._post('/register', data=data, content_type='application/json')
data = json.loads(r.data)
self.assertEquals(data['meta']['code'], 200)
def test_register_existing_email(self):
data = dict(email='matt@lp.com',
password='password',
password_confirm='password')
r = self._post('/register', data=data, follow_redirects=True)
msg = b'matt@lp.com is already associated with an account'
self.assertIn(msg, r.data)
def test_unauthorized(self):
self.authenticate("joe@lp.com", endpoint="/custom_auth")
r = self._get("/admin", follow_redirects=True)
msg = b'You are not allowed to access the requested resouce'
self.assertIn(msg, r.data)
def test_default_http_auth_realm(self):
r = self._get('/http', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus")
})
self.assertIn(b'<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Custom Realm"',
r.headers['WWW-Authenticate'])
class BadConfiguredSecurityTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORD_HASH': 'bcrypt',
'USER_COUNT': 1
}
def test_bad_configuration_raises_runtimer_error(self):
self.assertRaises(RuntimeError, self.authenticate)
class DefaultTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_LOGIN_USER_TEMPLATE': 'custom_security/login_user.html',
}
def test_login_user_template(self):
r = self._get('/login')
self.assertIn(b'CUSTOM LOGIN USER', r.data)
class RegisterableTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True,
'SECURITY_REGISTER_USER_TEMPLATE': 'custom_security/register_user.html'
}
def test_register_user_template(self):
r = self._get('/register')
self.assertIn(b'CUSTOM REGISTER USER', r.data)
class RecoverableTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_FORGOT_PASSWORD_TEMPLATE': 'custom_security/forgot_password.html',
'SECURITY_RESET_PASSWORD_TEMPLATE': 'custom_security/reset_password.html',
}
def test_forgot_password_template(self):
r = self._get('/reset')
self.assertIn(b'CUSTOM FORGOT PASSWORD', r.data)
def test_reset_password_template(self):
with capture_reset_password_requests() as requests:
r = self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._get('/reset/' + t)
self.assertIn(b'CUSTOM RESET PASSWORD', r.data)
class ConfirmableTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_SEND_CONFIRMATION_TEMPLATE': 'custom_security/send_confirmation.html'
}
def test_send_confirmation_template(self):
r = self._get('/confirm')
self.assertIn(b'CUSTOM SEND CONFIRMATION', r.data)
class PasswordlessTemplatePathTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True,
'SECURITY_SEND_LOGIN_TEMPLATE': 'custom_security/send_login.html'
}
def test_send_login_template(self):
r = self._get('/login')
self.assertIn(b'CUSTOM SEND LOGIN', r.data)
class RegisterableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_REGISTERABLE': True,
'USER_COUNT': 1
}
def test_register_valid_user(self):
data = dict(email='dude@lp.com',
password='password',
password_confirm='password')
self._post('/register', data=data, follow_redirects=True)
r = self.authenticate('dude@lp.com')
self.assertIn(b'Hello dude@lp.com', r.data)
class ConfirmableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'SECURITY_EMAIL_SUBJECT_REGISTER': 'Custom welcome subject',
'USER_COUNT': 1
}
def test_login_before_confirmation(self):
e = 'dude@lp.com'
self.register(e)
r = self.authenticate(email=e)
self.assertIn(self.get_message('CONFIRMATION_REQUIRED').encode('utf-8'), r.data)
def test_send_confirmation_of_already_confirmed_account(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
r = self.register(e)
token = registrations[0]['confirm_token']
self.client.get('/confirm/' + token, follow_redirects=True)
self.logout()
r = self._post('/confirm', data=dict(email=e))
m = self.get_message('ALREADY_CONFIRMED')
self.assertIn(m.encode('utf-8'), r.data)
def test_register_sends_confirmation_email(self):
e = 'dude@lp.com'
with self.app.extensions['mail'].record_messages() as outbox:
self.register(e)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
self.assertEqual('Custom welcome subject', outbox[0].subject)
def test_confirm_email(self):
e = 'dude@lp.com'
tokens = []
def on_registered(sender, **kwargs):
tokens.append(kwargs['confirm_token'])
user_registered.connect(on_registered, self.app)
r = self.register(e)
self.assertEqual(len(tokens), 1)
r = self.client.get('/confirm/' + tokens[0], follow_redirects=True)
msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
self.assertIn(msg.encode('utf-8'), r.data)
def test_invalid_token_when_confirming_email(self):
r = self.client.get('/confirm/bogus', follow_redirects=True)
msg = self.app.config['SECURITY_MSG_INVALID_CONFIRMATION_TOKEN'][0]
self.assertIn(msg.encode('utf-8'), r.data)
def test_send_confirmation_json(self):
r = self._post('/confirm', data='{"email": "matt@lp.com"}',
content_type='application/json')
self.assertEquals(r.status_code, 200)
def test_send_confirmation_with_invalid_email(self):
r = self._post('/confirm', data=dict(email='bogus@bogus.com'))
msg = self.app.config['SECURITY_MSG_USER_DOES_NOT_EXIST'][0]
self.assertIn(msg.encode('utf-8'), r.data)
def test_resend_confirmation(self):
e = 'dude@lp.com'
self.register(e)
r = self._post('/confirm', data={'email': e})
msg = self.get_message('CONFIRMATION_REQUEST', email=e).encode('utf-8')
self.assertIn(msg, r.data)
def test_user_deleted_before_confirmation(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
user = registrations[0]['user']
token = registrations[0]['confirm_token']
with self.app.app_context():
from flask_security.core import _security
_security.datastore.delete(user)
_security.datastore.commit()
r = self.client.get('/confirm/' + token, follow_redirects=True)
msg = self.app.config['SECURITY_MSG_INVALID_CONFIRMATION_TOKEN'][0]
self.assertIn(msg.encode('utf-8'), r.data)
class ExpiredConfirmationTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'SECURITY_CONFIRM_EMAIL_WITHIN': '1 milliseconds',
'USER_COUNT': 1
}
def test_expired_confirmation_token_sends_email(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
time.sleep(1.25)
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.get('/confirm/' + token, follow_redirects=True)
self.assertEqual(len(outbox), 1)
self.assertNotIn(token, outbox[0].html)
expire_text = self.AUTH_CONFIG['SECURITY_CONFIRM_EMAIL_WITHIN']
msg = self.app.config['SECURITY_MSG_CONFIRMATION_EXPIRED'][0]
msg = msg % dict(within=expire_text, email=e)
self.assertIn(msg.encode('utf-8'), r.data)
class LoginWithoutImmediateConfirmTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
'SECURITY_LOGIN_WITHOUT_CONFIRMATION': True,
'USER_COUNT': 1
}
def test_register_valid_user_automatically_signs_in(self):
e = 'dude@lp.com'
p = 'password'
data = dict(email=e, password=p, password_confirm=p)
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn(e.encode('utf-8'), r.data)
def test_confirm_email_of_user_different_than_current_user(self):
e1 = 'dude@lp.com'
e2 = 'lady@lp.com'
with capture_registrations() as registrations:
self.register(e1)
self.register(e2)
token1 = registrations[0]['confirm_token']
token2 = registrations[1]['confirm_token']
self.client.get('/confirm/' + token1, follow_redirects=True)
self.client.get('/logout')
self.authenticate(email=e1)
r = self.client.get('/confirm/' + token2, follow_redirects=True)
m = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
self.assertIn(m.encode('utf-8'), r.data)
self.assertIn(b'Hello lady@lp.com', r.data)
def test_login_unconfirmed_user_when_login_without_confirmation_is_true(self):
e = 'dude@lp.com'
p = 'password'
data = dict(email=e, password=p, password_confirm=p)
r = self._post('/register', data=data, follow_redirects=True)
self.assertIn(e.encode('utf-8'), r.data)
self.client.get('/logout')
r = self.authenticate(email=e)
self.assertIn(e.encode('utf-8'), r.data)
class RecoverableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/',
'SECURITY_POST_FORGOT_VIEW': '/'
}
def test_reset_view(self):
with capture_reset_password_requests() as requests:
r = self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._get('/reset/' + t)
self.assertIn(b'<h1>Reset password</h1>', r.data)
def test_forgot_post_sends_email(self):
with capture_reset_password_requests():
with self.app.extensions['mail'].record_messages() as outbox:
self._post('/reset', data=dict(email='joe@lp.com'))
self.assertEqual(len(outbox), 1)
def test_forgot_password_json(self):
r = self._post('/reset', data='{"email": "matt@lp.com"}',
content_type="application/json")
self.assertEquals(r.status_code, 200)
def test_forgot_password_invalid_email(self):
r = self._post('/reset', data=dict(email='larry@lp.com'),
follow_redirects=True)
self.assertIn(b"Specified user does not exist", r.data)
def test_reset_password_with_valid_token(self):
with capture_reset_password_requests() as requests:
r = self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
r = self._post('/reset/' + t, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
r = self.logout()
r = self.authenticate('joe@lp.com', 'newpassword')
self.assertIn(b'Hello joe@lp.com', r.data)
def test_reset_password_with_invalid_token(self):
r = self._post('/reset/bogus', data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
m = self.get_message('INVALID_RESET_PASSWORD_TOKEN')
self.assertIn(m.encode('utf-8'), r.data)
class ExpiredResetPasswordTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_WITHIN': '1 milliseconds'
}
def test_reset_password_with_expired_token(self):
with capture_reset_password_requests() as requests:
r = self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
t = requests[0]['token']
time.sleep(1)
r = self._post('/reset/' + t, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertIn(b'You did not reset your password within', r.data)
class ChangePasswordTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_CHANGEABLE': True,
}
def test_change_password(self):
self.authenticate()
r = self.client.get('/change', follow_redirects=True)
self.assertIn(b'Change password', r.data)
def test_change_password_invalid(self):
self.authenticate()
r = self._post('/change', data={
'password': 'notpassword',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
self.assertNotIn(b'You successfully changed your password', r.data)
self.assertIn(b'Invalid password', r.data)
def test_change_password_mismatch(self):
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'notnewpassword'
}, follow_redirects=True)
self.assertNotIn(b'You successfully changed your password', r.data)
self.assertIn(b'Passwords do not match', r.data)
def test_change_password_bad_password(self):
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'new_password': 'a',
'new_password_confirm': 'a'
}, follow_redirects=True)
self.assertNotIn(b'You successfully changed your password', r.data)
self.assertIn(b'Password must be at least 6 characters', r.data)
def test_change_password_same_as_previous(self):
self.authenticate()
r = self._post('/change', data={
'password': 'password',
'new_password': 'password',
'new_password_confirm': 'password'
}, follow_redirects=True)
self.assertNotIn(b'You successfully changed your password', r.data)
self.assertIn(b'Your new password must be different than your previous password.', r.data)
def test_change_password_success(self):
data = {
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}
self.authenticate()
with self.app.extensions['mail'].record_messages() as outbox:
r = self._post('/change', data=data, follow_redirects=True)
self.assertIn(b'You successfully changed your password', r.data)
self.assertIn(b'Home Page', r.data)
self.assertEqual(len(outbox), 1)
self.assertIn("Your password has been changed", outbox[0].html)
self.assertIn("/reset", outbox[0].html)
class EmailConfigTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_SEND_REGISTER_EMAIL': False,
'SECURITY_SEND_PASSWORD_CHANGE_EMAIL': False,
}
def test_change_password_success_email_option(self):
"""Test the change password email can be turned off w/ configuration."""
data = {
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}
self.authenticate()
with self.app.extensions['mail'].record_messages() as outbox:
self._post('/change', data=data, follow_redirects=True)
self.assertEqual(len(outbox), 0)
class ChangePasswordPostViewTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CHANGEABLE': True,
'SECURITY_POST_CHANGE_VIEW': '/profile',
}
def test_change_password_success(self):
data = {
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}
self.authenticate()
r = self._post('/change', data=data, follow_redirects=True)
self.assertIn(b'Profile Page', r.data)
class ChangePasswordDisabledTest(SecurityTest):
AUTH_CONFIG = {
'SECURITY_CHANGEABLE': False,
}
def test_change_password_endpoint_is_404(self):
self.authenticate()
r = self.client.get('/change', follow_redirects=True)
self.assertEqual(404, r.status_code)
class TrackableTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_TRACKABLE': True,
'USER_COUNT': 1
}
def test_did_track(self):
e = 'matt@lp.com'
self.authenticate(email=e)
self.logout()
self.authenticate(email=e)
with self.app.test_request_context('/profile'):
user = self.app.security.datastore.find_user(email=e)
self.assertIsNotNone(user.last_login_at)
self.assertIsNotNone(user.current_login_at)
self.assertEquals('untrackable', user.last_login_ip)
self.assertEquals('untrackable', user.current_login_ip)
self.assertEquals(2, user.login_count)
class PasswordlessTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True
}
def test_login_request_for_inactive_user(self):
msg = self.app.config['SECURITY_MSG_DISABLED_ACCOUNT'][0]
r = self._post('/login', data=dict(email='tiya@lp.com'),
follow_redirects=True)
self.assertIn(msg.encode('utf-8'), r.data)
def test_request_login_token_with_json_and_valid_email(self):
data = '{"email": "matt@lp.com", "password": "password"}'
r = self._post('/login', data=data, content_type='application/json')
self.assertEquals(r.status_code, 200)
self.assertNotIn(b'error', r.data)
def test_request_login_token_with_json_and_invalid_email(self):
data = '{"email": "nobody@lp.com", "password": "password"}'
r = self._post('/login', data=data, content_type='application/json')
self.assertIn(b'errors', r.data)
def test_request_login_token_sends_email_and_can_login(self):
e = 'matt@lp.com'
r, user, token = None, None, None
with capture_passwordless_login_requests() as requests:
with self.app.extensions['mail'].record_messages() as outbox:
r = self._post('/login', data=dict(email=e),
follow_redirects=True)
self.assertEqual(len(outbox), 1)
self.assertEquals(1, len(requests))
self.assertIn('user', requests[0])
self.assertIn('login_token', requests[0])
user = requests[0]['user']
token = requests[0]['login_token']
msg = self.app.config['SECURITY_MSG_LOGIN_EMAIL_SENT'][0]
msg = msg % dict(email=user.email)
self.assertIn(msg.encode('utf-8'), r.data)
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL').encode('utf-8')
self.assertIn(msg, r.data)
r = self.client.get('/profile')
self.assertIn(b'Profile Page', r.data)
def test_invalid_login_token(self):
m = self.app.config['SECURITY_MSG_INVALID_LOGIN_TOKEN'][0]
r = self._get('/login/bogus', follow_redirects=True)
self.assertIn(m.encode('utf-8'), r.data)
def test_token_login_when_already_authenticated(self):
with capture_passwordless_login_requests() as requests:
self._post('/login', data=dict(email='matt@lp.com'),
follow_redirects=True)
token = requests[0]['login_token']
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
self.assertIn(msg.encode('utf-8'), r.data)
r = self.client.get('/login/' + token, follow_redirects=True)
msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
self.assertNotIn(msg.encode('utf-8'), r.data)
def test_send_login_with_invalid_email(self):
r = self._post('/login', data=dict(email='bogus@bogus.com'))
self.assertIn(b'Specified user does not exist', r.data)
class ExpiredLoginTokenTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True,
'SECURITY_LOGIN_WITHIN': '1 milliseconds',
'USER_COUNT': 1
}
def test_expired_login_token_sends_email(self):
e = 'matt@lp.com'
with capture_passwordless_login_requests() as requests:
self._post('/login', data=dict(email=e), follow_redirects=True)
token = requests[0]['login_token']
time.sleep(1.25)
with self.app.extensions['mail'].record_messages() as outbox:
r = self.client.get('/login/' + token, follow_redirects=True)
expire_text = self.AUTH_CONFIG['SECURITY_LOGIN_WITHIN']
msg = self.app.config['SECURITY_MSG_LOGIN_EXPIRED'][0]
msg = msg % dict(within=expire_text, email=e)
self.assertIn(msg.encode('utf-8'), r.data)
self.assertEqual(len(outbox), 1)
self.assertIn(e, outbox[0].html)
self.assertNotIn(token, outbox[0].html)
class AsyncMailTaskTests(SecurityTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'USER_COUNT': 1
}
def setUp(self):
super(AsyncMailTaskTests, self).setUp()
self.mail_sent = False
def test_send_email_task_is_called(self):
@self.app.security.send_mail_task
def send_email(msg):
self.mail_sent = True
self._post('/reset', data=dict(email='matt@lp.com'))
self.assertTrue(self.mail_sent)
class NoBlueprintTests(SecurityTest):
APP_KWARGS = {
'register_blueprint': False,
}
AUTH_CONFIG = {
'USER_COUNT': 1
}
def test_login_endpoint_is_404(self):
r = self._get('/login')
self.assertEqual(404, r.status_code)
def test_http_auth_without_blueprint(self):
auth = base64.b64encode(b"matt@lp.com:password").decode('utf-8')
r = self._get('/http', headers={'Authorization': 'basic %s' % auth})
self.assertIn(b'HTTP Authentication', r.data)
class ExtendFormsTest(SecurityTest):
class MyLoginForm(LoginForm):
email = TextField('My Login Email Address Field')
class MyRegisterForm(RegisterForm):
email = TextField('My Register Email Address Field')
APP_KWARGS = {
'login_form': MyLoginForm,
'register_form': MyRegisterForm,
}
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': False,
'SECURITY_REGISTERABLE': True,
}
def test_login_view(self):
r = self._get('/login', follow_redirects=True)
self.assertIn(b"My Login Email Address Field", r.data)
def test_register(self):
r = self._get('/register', follow_redirects=True)
self.assertIn(b"My Register Email Address Field", r.data)
class RecoverableExtendFormsTest(SecurityTest):
class MyForgotPasswordForm(ForgotPasswordForm):
email = TextField('My Forgot Password Email Address Field',
validators=[valid_user_email])
class MyResetPasswordForm(ResetPasswordForm):
submit = SubmitField("My Reset Password Submit Field")
APP_KWARGS = {
'forgot_password_form': MyForgotPasswordForm,
'reset_password_form': MyResetPasswordForm,
}
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
}
def test_forgot_password(self):
r = self._get('/reset', follow_redirects=True)
self.assertIn(b"My Forgot Password Email Address Field", r.data)
def test_reset_password(self):
with capture_reset_password_requests() as requests:
self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
token = requests[0]['token']
r = self._get('/reset/' + token)
self.assertIn(b"My Reset Password Submit Field", r.data)
class PasswordlessExtendFormsTest(SecurityTest):
class MyPasswordlessLoginForm(PasswordlessLoginForm):
email = TextField('My Passwordless Login Email Address Field')
APP_KWARGS = {
'passwordless_login_form': MyPasswordlessLoginForm,
}
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True,
}
def test_passwordless_login(self):
r = self._get('/login', follow_redirects=True)
self.assertIn(b"My Passwordless Login Email Address Field", r.data)
class ConfirmableExtendFormsTest(SecurityTest):
class MyConfirmRegisterForm(ConfirmRegisterForm):
email = TextField('My Confirm Register Email Address Field')
class MySendConfirmationForm(SendConfirmationForm):
email = TextField('My Send Confirmation Email Address Field')
APP_KWARGS = {
'confirm_register_form': MyConfirmRegisterForm,
'send_confirmation_form': MySendConfirmationForm,
}
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
}
def test_register(self):
r = self._get('/register', follow_redirects=True)
self.assertIn(b"My Confirm Register Email Address Field", r.data)
def test_send_confirmation(self):
r = self._get('/confirm', follow_redirects=True)
self.assertIn(b"My Send Confirmation Email Address Field", r.data)
class AdditionalUserIdentityAttributes(SecurityTest):
AUTH_CONFIG = {
'SECURITY_USER_IDENTITY_ATTRIBUTES': ('email', 'username')
}
def test_authenticate(self):
r = self.authenticate(email='matt')
self.assertIn(b'Hello matt@lp.com', r.data)
+287
View File
@@ -0,0 +1,287 @@
# -*- coding: utf-8 -*-
"""
conftest
~~~~~~~~
Test fixtures and what not
"""
import os
import tempfile
import time
import pytest
from flask import Flask, render_template
from flask_mail import Mail
from flask_security import Security, MongoEngineUserDatastore, SQLAlchemyUserDatastore, \
PeeweeUserDatastore, UserMixin, RoleMixin, http_auth_required, login_required, \
auth_token_required, auth_required, roles_required, roles_accepted
from utils import populate_data, Response
@pytest.fixture()
def app(request):
app = Flask(__name__)
app.response_class = Response
app.debug = True
app.config['SECRET_KEY'] = 'secret'
app.config['TESTING'] = True
app.config['LOGIN_DISABLED'] = False
app.config['WTF_CSRF_ENABLED'] = False
for opt in ['changeable', 'recoverable', 'registerable',
'trackable', 'passwordless', 'confirmable']:
app.config['SECURITY_' + opt.upper()] = opt in request.keywords
if 'settings' in request.keywords:
for key, value in request.keywords['settings'].kwargs.items():
app.config['SECURITY_' + key.upper()] = value
mail = Mail(app)
app.mail = mail
@app.route('/')
def index():
return render_template('index.html', content='Home Page')
@app.route('/profile')
@login_required
def profile():
return render_template('index.html', content='Profile Page')
@app.route('/post_login')
@login_required
def post_login():
return render_template('index.html', content='Post Login')
@app.route('/http')
@http_auth_required
def http():
return 'HTTP Authentication'
@app.route('/http_custom_realm')
@http_auth_required('My Realm')
def http_custom_realm():
return render_template('index.html', content='HTTP Authentication')
@app.route('/token', methods=['GET', 'POST'])
@auth_token_required
def token():
return render_template('index.html', content='Token Authentication')
@app.route('/multi_auth')
@auth_required('session', 'token', 'basic')
def multi_auth():
return render_template('index.html', content='Session, Token, Basic auth')
@app.route('/post_logout')
def post_logout():
return render_template('index.html', content='Post Logout')
@app.route('/post_register')
def post_register():
return render_template('index.html', content='Post Register')
@app.route('/admin')
@roles_required('admin')
def admin():
return render_template('index.html', content='Admin Page')
@app.route('/admin_and_editor')
@roles_required('admin', 'editor')
def admin_and_editor():
return render_template('index.html', content='Admin and Editor Page')
@app.route('/admin_or_editor')
@roles_accepted('admin', 'editor')
def admin_or_editor():
return render_template('index.html', content='Admin or Editor Page')
@app.route('/unauthorized')
def unauthorized():
return render_template('unauthorized.html')
@app.route('/page1')
def page_1():
return 'Page 1'
return app
@pytest.fixture()
def mongoengine_datastore(request, app):
from flask_mongoengine import MongoEngine
db_name = 'flask_security_test_%s' % str(time.time()).replace('.', '_')
app.config['MONGODB_SETTINGS'] = {
'db': db_name,
'host': 'localhost',
'port': 27017,
'alias': db_name
}
db = MongoEngine(app)
class Role(db.Document, RoleMixin):
name = db.StringField(required=True, unique=True, max_length=80)
description = db.StringField(max_length=255)
meta = {"db_alias": db_name}
class User(db.Document, UserMixin):
email = db.StringField(unique=True, max_length=255)
username = db.StringField(max_length=255)
password = db.StringField(required=False, max_length=255)
last_login_at = db.DateTimeField()
current_login_at = db.DateTimeField()
last_login_ip = db.StringField(max_length=100)
current_login_ip = db.StringField(max_length=100)
login_count = db.IntField()
active = db.BooleanField(default=True)
confirmed_at = db.DateTimeField()
roles = db.ListField(db.ReferenceField(Role), default=[])
meta = {"db_alias": db_name}
request.addfinalizer(lambda: db.connection.drop_database(db_name))
return MongoEngineUserDatastore(db, User, Role)
@pytest.fixture()
def sqlalchemy_datastore(request, app, tmpdir):
from flask_sqlalchemy import SQLAlchemy
f, path = tempfile.mkstemp(prefix='flask-security-test-db', suffix='.db', dir=str(tmpdir))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + path
db = SQLAlchemy(app)
roles_users = db.Table(
'roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
username = db.Column(db.String(255))
password = db.Column(db.String(255))
last_login_at = db.Column(db.DateTime())
current_login_at = db.Column(db.DateTime())
last_login_ip = db.Column(db.String(100))
current_login_ip = db.Column(db.String(100))
login_count = db.Column(db.Integer)
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
with app.app_context():
db.create_all()
request.addfinalizer(lambda: os.remove(path))
return SQLAlchemyUserDatastore(db, User, Role)
@pytest.fixture()
def peewee_datastore(request, app, tmpdir):
from peewee import TextField, DateTimeField, IntegerField, BooleanField, ForeignKeyField
from flask_peewee.db import Database
f, path = tempfile.mkstemp(prefix='flask-security-test-db', suffix='.db', dir=str(tmpdir))
app.config['DATABASE'] = {
'name': path,
'engine': 'peewee.SqliteDatabase'
}
db = Database(app)
class Role(db.Model, RoleMixin):
name = TextField(unique=True)
description = TextField(null=True)
class User(db.Model, UserMixin):
email = TextField()
username = TextField()
password = TextField(null=True)
last_login_at = DateTimeField(null=True)
current_login_at = DateTimeField(null=True)
last_login_ip = TextField(null=True)
current_login_ip = TextField(null=True)
login_count = IntegerField(null=True)
active = BooleanField(default=True)
confirmed_at = DateTimeField(null=True)
class UserRoles(db.Model):
""" Peewee does not have built-in many-to-many support, so we have to
create this mapping class to link users to roles."""
user = ForeignKeyField(User, related_name='roles')
role = ForeignKeyField(Role, related_name='users')
name = property(lambda self: self.role.name)
description = property(lambda self: self.role.description)
with app.app_context():
for Model in (Role, User, UserRoles):
Model.create_table()
request.addfinalizer(lambda: os.remove(path))
return PeeweeUserDatastore(db, User, Role, UserRoles)
@pytest.fixture()
def sqlalchemy_app(app, sqlalchemy_datastore):
def create():
app.security = Security(app, datastore=sqlalchemy_datastore)
return app
return create
@pytest.fixture()
def peewee_app(app, peewee_datastore):
def create():
app.security = Security(app, datastore=peewee_datastore)
return app
return create
@pytest.fixture()
def mongoengine_app(app, mongoengine_datastore):
def create():
app.security = Security(app, datastore=mongoengine_datastore)
return app
return create
@pytest.fixture()
def client(request, sqlalchemy_app):
app = sqlalchemy_app()
populate_data(app)
return app.test_client()
@pytest.fixture()
def get_message(app):
def fn(key, **kwargs):
rv = app.config['SECURITY_MSG_' + key][0] % kwargs
return rv.encode('utf-8')
return fn
@pytest.fixture(params=['sqlalchemy', 'mongoengine', 'peewee'])
def datastore(request, sqlalchemy_datastore, mongoengine_datastore, peewee_datastore):
if request.param == 'sqlalchemy':
rv = sqlalchemy_datastore
elif request.param == 'mongoengine':
rv = mongoengine_datastore
elif request.param == 'peewee':
rv = peewee_datastore
return rv
-277
View File
@@ -1,277 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
import base64
import simplejson as json
try:
from cookielib import Cookie
except ImportError:
from http.cookiejar import Cookie
from werkzeug.utils import parse_cookie
from tests import SecurityTest
def get_cookies(rv):
cookies = {}
for value in rv.headers.get_all("Set-Cookie"):
cookies.update(parse_cookie(value))
return cookies
class DefaultSecurityTests(SecurityTest):
def test_instance(self):
self.assertIsNotNone(self.app)
self.assertIsNotNone(self.app.security)
self.assertIsNotNone(self.app.security.pwd_context)
def test_login_view(self):
r = self._get('/login')
self.assertIn(b'<h1>Login</h1>', r.data)
def test_authenticate(self):
r = self.authenticate()
self.assertIn(b'Hello matt@lp.com', r.data)
def test_authenticate_case_insensitive_email(self):
r = self.authenticate(email='MATT@lp.com')
self.assertIn(b'Hello matt@lp.com', r.data)
def test_unprovided_username(self):
r = self.authenticate("")
self.assertIn(self.get_message('EMAIL_NOT_PROVIDED').encode('utf-8'), r.data)
def test_unprovided_password(self):
r = self.authenticate(password="")
self.assertIn(self.get_message('PASSWORD_NOT_PROVIDED').encode('utf-8'), r.data)
def test_invalid_user(self):
r = self.authenticate(email="bogus@bogus.com")
self.assertIn(self.get_message('USER_DOES_NOT_EXIST').encode('utf-8'), r.data)
def test_bad_password(self):
r = self.authenticate(password="bogus")
self.assertIn(self.get_message('INVALID_PASSWORD').encode('utf-8'), r.data)
def test_inactive_user(self):
r = self.authenticate("tiya@lp.com", "password")
self.assertIn(self.get_message('DISABLED_ACCOUNT').encode('utf-8'), r.data)
def test_logout(self):
self.authenticate()
r = self.logout()
self.assertIsHomePage(r.data)
def test_unauthorized_access(self):
self.logout()
r = self._get('/profile', follow_redirects=True)
self.assertIn(b'<li class="info">Please log in to access this page.</li>', r.data)
def test_authorized_access(self):
self.authenticate()
r = self._get("/profile")
self.assertIn(b'profile', r.data)
def test_valid_admin_role(self):
self.authenticate()
r = self._get("/admin")
self.assertIn(b'Admin Page', r.data)
def test_invalid_admin_role(self):
self.authenticate("joe@lp.com")
r = self._get("/admin", follow_redirects=True)
self.assertIsHomePage(r.data)
def test_roles_accepted(self):
for user in ("matt@lp.com", "joe@lp.com"):
self.authenticate(user)
r = self._get("/admin_or_editor")
self.assertIn(b'Admin or Editor Page', r.data)
self.logout()
self.authenticate("jill@lp.com")
r = self._get("/admin_or_editor", follow_redirects=True)
self.assertIsHomePage(r.data)
def test_unauthenticated_role_required(self):
r = self._get('/admin', follow_redirects=True)
self.assertIn(self.get_message('UNAUTHORIZED').encode('utf-8'), r.data)
def test_multiple_role_required(self):
for user in ("matt@lp.com", "joe@lp.com"):
self.authenticate(user)
r = self._get("/admin_and_editor", follow_redirects=True)
self.assertIsHomePage(r.data)
self._get('/logout')
self.authenticate('dave@lp.com')
r = self._get("/admin_and_editor", follow_redirects=True)
self.assertIn(b'Admin and Editor Page', r.data)
def test_ok_json_auth(self):
r = self.json_authenticate()
data = json.loads(r.data)
self.assertEquals(data['meta']['code'], 200)
self.assertIn('authentication_token', data['response']['user'])
def test_invalid_json_auth(self):
r = self.json_authenticate(password='junk')
self.assertIn(b'"code": 400', r.data)
def test_token_auth_via_querystring_valid_token(self):
r = self.json_authenticate()
data = json.loads(r.data)
token = data['response']['user']['authentication_token']
r = self._get('/token?auth_token=' + token)
self.assertIn(b'Token Authentication', r.data)
def test_token_auth_via_header_valid_token(self):
r = self.json_authenticate()
data = json.loads(r.data)
token = data['response']['user']['authentication_token']
headers = {"Authentication-Token": token}
r = self._get('/token', headers=headers)
self.assertIn(b'Token Authentication', r.data)
def test_token_auth_via_querystring_invalid_token(self):
r = self._get('/token?auth_token=X')
self.assertEqual(401, r.status_code)
def test_token_auth_via_header_invalid_token(self):
r = self._get('/token', headers={"Authentication-Token": 'X'})
self.assertEqual(401, r.status_code)
def test_http_auth(self):
r = self._get('/http', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:password").decode('utf-8')
})
self.assertIn(b'HTTP Authentication', r.data)
def test_http_auth_no_authorization(self):
r = self._get('/http', headers={})
self.assertIn(b'<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Login Required"',
r.headers['WWW-Authenticate'])
def test_invalid_http_auth_invalid_username(self):
r = self._get('/http', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"bogus:bogus").decode('utf-8')
})
self.assertIn(b'<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Login Required"',
r.headers['WWW-Authenticate'])
def test_invalid_http_auth_bad_password(self):
r = self._get('/http', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus").decode('utf-8')
})
self.assertIn(b'<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="Login Required"',
r.headers['WWW-Authenticate'])
def test_custom_http_auth_realm(self):
r = self._get('/http_custom_realm', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus").decode('utf-8')
})
self.assertIn(b'<h1>Unauthorized</h1>', r.data)
self.assertIn('WWW-Authenticate', r.headers)
self.assertEquals('Basic realm="My Realm"',
r.headers['WWW-Authenticate'])
def test_multi_auth_basic(self):
r = self._get('/multi_auth', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:password").decode('utf-8')
})
self.assertIn(b'Basic', r.data)
def test_multi_auth_token(self):
r = self.json_authenticate()
data = json.loads(r.data)
token = data['response']['user']['authentication_token']
r = self._get('/multi_auth?auth_token=' + token)
self.assertIn(b'Token', r.data)
def test_multi_auth_session(self):
self.authenticate()
r = self._get('/multi_auth')
self.assertIn(b'Session', r.data)
def test_user_deleted_during_session_reverts_to_anonymous_user(self):
self.authenticate()
with self.app.test_request_context('/'):
user = self.app.security.datastore.find_user(email='matt@lp.com')
self.app.security.datastore.delete_user(user)
self.app.security.datastore.commit()
r = self._get('/')
self.assertNotIn(b'Hello matt@lp.com', r.data)
def test_remember_token(self):
r = self.authenticate(follow_redirects=False)
self.client.cookie_jar.clear_session_cookies()
r = self._get('/profile')
self.assertIn(b'profile', r.data)
def test_token_loader_does_not_fail_with_invalid_token(self):
c = Cookie(version=0, name='remember_token', value='None', port=None,
port_specified=False, domain='www.example.com',
domain_specified=False, domain_initial_dot=False, path='/',
path_specified=True, secure=False, expires=None,
discard=True, comment=None, comment_url=None,
rest={'HttpOnly': None}, rfc2109=False)
self.client.cookie_jar.set_cookie(c)
r = self._get('/')
self.assertNotIn(b'BadSignature', r.data)
class MongoEngineSecurityTests(DefaultSecurityTests):
def _create_app(self, auth_config, **kwargs):
from tests.test_app.mongoengine import create_app
return create_app(auth_config, **kwargs)
class PeeweeSecurityTests(DefaultSecurityTests):
def _create_app(self, auth_config, **kwargs):
from tests.test_app.peewee_app import create_app
return create_app(auth_config, **kwargs)
class DefaultDatastoreTests(SecurityTest):
def test_add_role_to_user(self):
r = self._get('/coverage/add_role_to_user')
self.assertIn(b'success', r.data)
def test_remove_role_from_user(self):
r = self._get('/coverage/remove_role_from_user')
self.assertIn(b'success', r.data)
def test_activate_user(self):
r = self._get('/coverage/activate_user')
self.assertIn(b'success', r.data)
def test_deactivate_user(self):
r = self._get('/coverage/deactivate_user')
self.assertIn(b'success', r.data)
def test_invalid_role(self):
r = self._get('/coverage/invalid_role')
self.assertIn(b'success', r.data)
class MongoEngineDatastoreTests(DefaultDatastoreTests):
def _create_app(self, auth_config, **kwargs):
from tests.test_app.mongoengine import create_app
return create_app(auth_config, **kwargs)
-244
View File
@@ -1,244 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
from flask_security.utils import capture_registrations, \
capture_reset_password_requests, capture_signals
from flask_security.signals import user_registered, user_confirmed, \
confirm_instructions_sent, login_instructions_sent, \
password_reset, password_changed, reset_password_instructions_sent
from tests import SecurityTest
def compare_user(a, b):
"""Helper to compare two users."""
return a.id == b.id and a.email == b.email and a.password == b.password
class SignalTest(SecurityTest):
def _create_app(self, auth_config, **kwargs):
from tests.test_app.mongoengine import create_app
return create_app(auth_config, **kwargs)
class RegisterableSignalsTests(SignalTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
}
def test_register(self):
e = 'dude@lp.com'
with capture_signals() as mocks:
self.register(e)
user = self.app.security.datastore.find_user(email='dude@lp.com')
self.assertEqual(mocks.signals_sent(), set([user_registered]))
calls = mocks[user_registered]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertIn('confirm_token', kwargs)
self.assertEqual(args[0], self.app)
def test_register_without_password(self):
e = 'dude@lp.com'
with capture_signals() as mocks:
self.register(e, password='')
self.assertEqual(mocks.signals_sent(), set())
class ConfirmableSignalsTests(SignalTest):
AUTH_CONFIG = {
'SECURITY_CONFIRMABLE': True,
'SECURITY_REGISTERABLE': True,
}
def test_confirm(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
with capture_signals() as mocks:
self.client.get('/confirm/' + token, follow_redirects=True)
user = self.app.security.datastore.find_user(email='dude@lp.com')
self.assertTrue(mocks.signals_sent(), set([user_confirmed]))
calls = mocks[user_confirmed]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertEqual(args[0], self.app)
self.assertTrue(compare_user(kwargs['user'], user))
def test_confirm_bad_token(self):
e = 'dude@lp.com'
with capture_registrations():
self.register(e)
with capture_signals() as mocks:
self.client.get('/confirm/bogus', follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_confirm_twice(self):
e = 'dude@lp.com'
with capture_registrations() as registrations:
self.register(e)
token = registrations[0]['confirm_token']
self.client.get('/confirm/' + token, follow_redirects=True)
self.logout()
with capture_signals() as mocks:
self.client.get('/confirm/' + token, follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([user_confirmed]))
# TODO: is that the desired behaviour?
def test_resend_confirmation(self):
e = 'dude@lp.com'
self.register(e)
with capture_signals() as mocks:
self._post('/confirm', data={'email': e})
user = self.app.security.datastore.find_user(email='dude@lp.com')
self.assertEqual(mocks.signals_sent(), set([confirm_instructions_sent]))
calls = mocks[confirm_instructions_sent]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertEqual(args[0], self.app)
def test_send_confirmation_bad_email(self):
with capture_signals() as mocks:
self._post('/confirm', data=dict(email='bogus@bogus.com'))
self.assertEqual(mocks.signals_sent(), set())
class RecoverableSignalsTests(SignalTest):
AUTH_CONFIG = {
'SECURITY_RECOVERABLE': True,
'SECURITY_RESET_PASSWORD_ERROR_VIEW': '/',
'SECURITY_POST_FORGOT_VIEW': '/'
}
def test_reset_password_request(self):
with capture_signals() as mocks:
self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([reset_password_instructions_sent]))
user = self.app.security.datastore.find_user(email='joe@lp.com')
calls = mocks[reset_password_instructions_sent]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertIn('token', kwargs)
self.assertEqual(args[0], self.app)
def test_reset_password(self):
with capture_reset_password_requests() as requests:
self._post('/reset', data=dict(email='joe@lp.com'),
follow_redirects=True)
token = requests[0]['token']
with capture_signals() as mocks:
data = dict(password='newpassword', password_confirm='newpassword')
self._post('/reset/' + token, data, follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([password_reset]))
user = self.app.security.datastore.find_user(email='joe@lp.com')
calls = mocks[password_reset]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertEqual(args[0], self.app)
def test_reset_password_invalid_emails(self):
with capture_signals() as mocks:
self._post('/reset', data=dict(email='nobody@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_reset_password_invalid_token(self):
with capture_signals() as mocks:
data = dict(password='newpassword', password_confirm='newpassword')
self._post('/reset/bogus', data, follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
class ChangeableSignalsTests(SignalTest):
AUTH_CONFIG = {
'SECURITY_CHANGEABLE': True,
}
def test_change_password(self):
self.authenticate('joe@lp.com')
with capture_signals() as mocks:
with self.client as client:
client.post('/change',
data=dict(password='password',
new_password='newpassword',
new_password_confirm='newpassword'))
self.assertEqual(mocks.signals_sent(), set([password_changed]))
user = self.app.security.datastore.find_user(email='joe@lp.com')
calls = mocks[password_changed]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(args[0], user))
self.assertEqual(kwargs['app'], self.app)
def test_change_password_invalid_password(self):
with capture_signals() as mocks:
self.client.post('/change',
data=dict(password='notpassword',
new_password='newpassword',
new_password_confirm='newpassword'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_change_password_bad_password(self):
with capture_signals() as mocks:
self.client.post('/change',
data=dict(password='notpassword',
new_password='a',
new_password_confirm='a'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_change_password_mismatch_password(self):
with capture_signals() as mocks:
self.client.post('/change',
data=dict(password='password',
new_password='newpassword',
new_password_confirm='notnewpassword'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
class PasswordlessTests(SignalTest):
AUTH_CONFIG = {
'SECURITY_PASSWORDLESS': True
}
def test_login_request_for_inactive_user(self):
with capture_signals() as mocks:
self._post('/login', data=dict(email='tiya@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_login_request_for_invalid_email(self):
with capture_signals() as mocks:
self._post('/login', data=dict(email='nobody@lp.com'),
follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set())
def test_request_login_token_sends_email_and_can_login(self):
e = 'matt@lp.com'
with capture_signals() as mocks:
self._post('/login', data=dict(email=e), follow_redirects=True)
self.assertEqual(mocks.signals_sent(), set([login_instructions_sent]))
user = self.app.security.datastore.find_user(email='matt@lp.com')
calls = mocks[login_instructions_sent]
self.assertEqual(len(calls), 1)
args, kwargs = calls[0]
self.assertTrue(compare_user(kwargs['user'], user))
self.assertIn('login_token', kwargs)
self.assertEqual(args[0], self.app)
@@ -1,4 +1,4 @@
{%- if current_user.is_authenticated() -%} {%- if current_user.is_authenticated -%}
<p>Hello {{ current_user.email }}</p> <p>Hello {{ current_user.email }}</p>
{%- endif %} {%- endif %}
<ul> <ul>
@@ -11,7 +11,7 @@
<li><a href="{{ url_for('admin_or_editor') }}">Admin or Editor</a></li> <li><a href="{{ url_for('admin_or_editor') }}">Admin or Editor</a></li>
{% endif -%} {% endif -%}
<li> <li>
{%- if current_user.is_authenticated() -%} {%- if current_user.is_authenticated -%}
<a href="{{ url_for('security.logout') }}">Log out</a> <a href="{{ url_for('security.logout') }}">Log out</a>
{%- else -%} {%- else -%}
<a href="{{ url_for('security.login') }}">Log in</a> <a href="{{ url_for('security.login') }}">Log in</a>
@@ -0,0 +1,3 @@
CUSTOM CHANGE PASSWORD
{{ global }}
{{ foo }}
@@ -1 +1,3 @@
CUSTOM FORGOT PASSWORD CUSTOM FORGOT PASSWORD
{{ global }}
{{ foo }}
@@ -0,0 +1,3 @@
CUSTOM LOGIN USER
{{ global }}
{{ foo }}
@@ -0,0 +1,3 @@
CUSTOM REGISTER USER
{{ global }}
{{ foo }}
@@ -0,0 +1,3 @@
CUSTOM RESET PASSWORD
{{ global }}
{{ foo }}
@@ -1 +1,3 @@
CUSTOM SEND CONFIRMATION CUSTOM SEND CONFIRMATION
{{ global }}
{{ foo }}
@@ -0,0 +1,3 @@
CUSTOM SEND LOGIN
{{ global }}
{{ foo }}
@@ -0,0 +1,3 @@
CUSTOM RESET INSTRUCTIONS
{{ global }}
{{ foo }}
-185
View File
@@ -1,185 +0,0 @@
# -*- coding: utf-8 -*-
from flask import Flask, render_template, current_app
from flask.ext.mail import Mail
from flask.ext.security import login_required, roles_required, roles_accepted
from flask.ext.security.decorators import http_auth_required, \
auth_token_required, auth_required
from flask.ext.security.utils import encrypt_password
from werkzeug.local import LocalProxy
ds = LocalProxy(lambda: current_app.extensions['security'].datastore)
def create_app(config):
app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'secret'
app.config['TESTING'] = True
app.config['LOGIN_DISABLED'] = False
for key, value in config.items():
app.config[key] = value
mail = Mail(app)
app.extensions['mail'] = mail
@app.route('/')
def index():
return render_template('index.html', content='Home Page')
@app.route('/profile')
@login_required
def profile():
return render_template('index.html', content='Profile Page')
@app.route('/post_login')
@login_required
def post_login():
return render_template('index.html', content='Post Login')
@app.route('/http')
@http_auth_required
def http():
return 'HTTP Authentication'
@app.route('/http_custom_realm')
@http_auth_required('My Realm')
def http_custom_realm():
return render_template('index.html', content='HTTP Authentication')
@app.route('/token')
@auth_token_required
def token():
return render_template('index.html', content='Token Authentication')
@app.route('/multi_auth')
@auth_required('session', 'token', 'basic')
def multi_auth():
return render_template('index.html', content='Session, Token, Basic auth')
@app.route('/post_logout')
def post_logout():
return render_template('index.html', content='Post Logout')
@app.route('/post_register')
def post_register():
return render_template('index.html', content='Post Register')
@app.route('/admin')
@roles_required('admin')
def admin():
return render_template('index.html', content='Admin Page')
@app.route('/admin_and_editor')
@roles_required('admin', 'editor')
def admin_and_editor():
return render_template('index.html', content='Admin and Editor Page')
@app.route('/admin_or_editor')
@roles_accepted('admin', 'editor')
def admin_or_editor():
return render_template('index.html', content='Admin or Editor Page')
@app.route('/unauthorized')
def unauthorized():
return render_template('unauthorized.html')
@app.route('/coverage/add_role_to_user')
def add_role_to_user():
u = ds.find_user(email='joe@lp.com')
r = ds.find_role('admin')
ds.add_role_to_user(u, r)
return 'success'
@app.route('/coverage/remove_role_from_user')
def remove_role_from_user():
u = ds.find_user(email='matt@lp.com')
ds.remove_role_from_user(u, 'admin')
return 'success'
@app.route('/coverage/deactivate_user')
def deactivate_user():
u = ds.find_user(email='matt@lp.com')
ds.deactivate_user(u)
return 'success'
@app.route('/coverage/activate_user')
def activate_user():
u = ds.find_user(email='tiya@lp.com')
ds.activate_user(u)
return 'success'
@app.route('/coverage/invalid_role')
def invalid_role():
return 'success' if ds.find_role('bogus') is None else 'failure'
@app.route('/page1')
def page_1():
return 'Page 1'
return app
def create_roles():
for role in ('admin', 'editor', 'author'):
ds.create_role(name=role)
ds.commit()
def create_users(count=None):
users = [('matt@lp.com', 'matt', 'password', ['admin'], True),
('joe@lp.com', 'joe', 'password', ['editor'], True),
('dave@lp.com', 'dave', 'password', ['admin', 'editor'], True),
('jill@lp.com', 'jill', 'password', ['author'], True),
('tiya@lp.com', 'tiya', 'password', [], False)]
count = count or len(users)
for u in users[:count]:
pw = encrypt_password(u[2])
roles = [ds.find_or_create_role(rn) for rn in u[3]]
ds.commit()
user = ds.create_user(email=u[0], username=u[1], password=pw, active=u[4])
ds.commit()
for role in roles:
ds.add_role_to_user(user, role)
ds.commit()
def populate_data(user_count=None):
create_roles()
create_users(user_count)
def add_context_processors(s):
@s.context_processor
def for_all():
return dict()
@s.forgot_password_context_processor
def forgot_password():
return dict()
@s.login_context_processor
def login():
return dict()
@s.register_context_processor
def register():
return dict()
@s.reset_password_context_processor
def reset_password():
return dict()
@s.send_confirmation_context_processor
def send_confirmation():
return dict()
@s.send_login_context_processor
def send_login():
return dict()
@s.mail_context_processor
def mail():
return dict()
-57
View File
@@ -1,57 +0,0 @@
# -*- coding: utf-8 -*-
import sys
import os
sys.path.pop(0)
sys.path.insert(0, os.getcwd())
from flask.ext.mongoengine import MongoEngine
from flask.ext.security import Security, UserMixin, RoleMixin, \
MongoEngineUserDatastore
from tests.test_app import create_app as create_base_app, populate_data, \
add_context_processors
def create_app(config, **kwargs):
app = create_base_app(config)
app.config['MONGODB_SETTINGS'] = dict(
db='flask_security_test',
host='localhost',
port=27017
)
db = MongoEngine(app)
class Role(db.Document, RoleMixin):
name = db.StringField(required=True, unique=True, max_length=80)
description = db.StringField(max_length=255)
class User(db.Document, UserMixin):
email = db.StringField(unique=True, max_length=255)
username = db.StringField(max_length=255)
password = db.StringField(required=True, max_length=255)
last_login_at = db.DateTimeField()
current_login_at = db.DateTimeField()
last_login_ip = db.StringField(max_length=100)
current_login_ip = db.StringField(max_length=100)
login_count = db.IntField()
active = db.BooleanField(default=True)
confirmed_at = db.DateTimeField()
roles = db.ListField(db.ReferenceField(Role), default=[])
@app.before_first_request
def before_first_request():
User.drop_collection()
Role.drop_collection()
populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, datastore=MongoEngineUserDatastore(db, User, Role), **kwargs)
add_context_processors(app.security)
return app
if __name__ == '__main__':
create_app({}).run()
-64
View File
@@ -1,64 +0,0 @@
# -*- coding: utf-8 -*-
import sys
import os
sys.path.pop(0)
sys.path.insert(0, os.getcwd())
from flask_peewee.db import Database
from peewee import *
from flask.ext.security import Security, UserMixin, RoleMixin, \
PeeweeUserDatastore
from tests.test_app import create_app as create_base_app, populate_data, \
add_context_processors
def create_app(config, **kwargs):
app = create_base_app(config)
app.config['DATABASE'] = {
'name': 'peewee.db',
'engine': 'peewee.SqliteDatabase'
}
db = Database(app)
class Role(db.Model, RoleMixin):
name = TextField(unique=True)
description = TextField(null=True)
class User(db.Model, UserMixin):
email = TextField()
username = TextField()
password = TextField()
last_login_at = DateTimeField(null=True)
current_login_at = DateTimeField(null=True)
last_login_ip = TextField(null=True)
current_login_ip = TextField(null=True)
login_count = IntegerField(null=True)
active = BooleanField(default=True)
confirmed_at = DateTimeField(null=True)
class UserRoles(db.Model):
""" Peewee does not have built-in many-to-many support, so we have to
create this mapping class to link users to roles."""
user = ForeignKeyField(User, related_name='roles')
role = ForeignKeyField(Role, related_name='users')
name = property(lambda self: self.role.name)
description = property(lambda self: self.role.description)
@app.before_first_request
def before_first_request():
for Model in (Role, User, UserRoles):
Model.drop_table(fail_silently=True)
Model.create_table()
populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, datastore=PeeweeUserDatastore(db, User, Role, UserRoles), **kwargs)
add_context_processors(app.security)
return app
if __name__ == '__main__':
create_app({}).run()
-60
View File
@@ -1,60 +0,0 @@
# -*- coding: utf-8 -*-
import sys
import os
sys.path.pop(0)
sys.path.insert(0, os.getcwd())
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.security import Security, UserMixin, RoleMixin, \
SQLAlchemyUserDatastore
from tests.test_app import create_app as create_base_app, populate_data, \
add_context_processors
def create_app(config, **kwargs):
app = create_base_app(config)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
db = SQLAlchemy(app)
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
username = db.Column(db.String(255))
password = db.Column(db.String(255))
last_login_at = db.Column(db.DateTime())
current_login_at = db.Column(db.DateTime())
last_login_ip = db.Column(db.String(100))
current_login_ip = db.Column(db.String(100))
login_count = db.Column(db.Integer)
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
@app.before_first_request
def before_first_request():
db.drop_all()
db.create_all()
populate_data(app.config.get('USER_COUNT', None))
app.security = Security(app, datastore=SQLAlchemyUserDatastore(db, User, Role), **kwargs)
add_context_processors(app.security)
return app
if __name__ == '__main__':
create_app({}).run()
@@ -1 +0,0 @@
CUSTOM LOGIN USER
@@ -1 +0,0 @@
CUSTOM REGISTER USER
@@ -1 +0,0 @@
CUSTOM RESET PASSWORD
@@ -1 +0,0 @@
CUSTOM SEND LOGIN
+131
View File
@@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
"""
test_changeable
~~~~~~~~~~~~~~~
Changeable tests
"""
import pytest
from flask import Flask
from flask_security.core import UserMixin
from flask_security.signals import password_changed
from utils import authenticate
pytestmark = pytest.mark.changeable()
def test_recoverable_flag(app, client, get_message):
recorded = []
@password_changed.connect_via(app)
def on_password_changed(app, user):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
recorded.append(user)
authenticate(client)
# Test change view
response = client.get('/change', follow_redirects=True)
assert b'Change password' in response.data
# Test wrong original password
response = client.post('/change', data={
'password': 'notpassword',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('INVALID_PASSWORD') in response.data
# Test mismatch
response = client.post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'notnewpassword'
}, follow_redirects=True)
assert get_message('RETYPE_PASSWORD_MISMATCH') in response.data
# Test missing password
response = client.post('/change', data={
'password': ' ',
'new_password': '',
'new_password_confirm': ''
}, follow_redirects=True)
assert get_message('PASSWORD_NOT_PROVIDED') in response.data
# Test bad password
response = client.post('/change', data={
'password': 'password',
'new_password': 'a',
'new_password_confirm': 'a'
}, follow_redirects=True)
assert get_message('PASSWORD_INVALID_LENGTH') in response.data
# Test same as previous
response = client.post('/change', data={
'password': 'password',
'new_password': 'password',
'new_password_confirm': 'password'
}, follow_redirects=True)
assert get_message('PASSWORD_IS_THE_SAME') in response.data
# Test successful submit sends email notification
with app.mail.record_messages() as outbox:
response = client.post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('PASSWORD_CHANGE') in response.data
assert b'Home Page' in response.data
assert len(recorded) == 1
assert len(outbox) == 1
assert "Your password has been changed" in outbox[0].html
# Test JSON
data = ('{"password": "newpassword", "new_password": "newpassword2", '
'"new_password_confirm": "newpassword2"}')
response = client.post('/change', data=data, headers={'Content-Type': 'application/json'})
assert response.status_code == 200
assert response.headers['Content-Type'] == 'application/json'
@pytest.mark.settings(change_url='/custom_change')
def test_custom_change_url(client):
authenticate(client)
response = client.get('/custom_change')
assert response.status_code == 200
@pytest.mark.settings(change_password_template='custom_security/change_password.html')
def test_custom_change_template(client):
authenticate(client)
response = client.get('/change')
assert b'CUSTOM CHANGE PASSWORD' in response.data
@pytest.mark.settings(send_password_change_email=False)
def test_disable_change_emails(app, client):
with app.mail.record_messages() as outbox:
client.post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
assert len(outbox) == 0
@pytest.mark.settings(post_change_view='/profile')
def test_custom_post_change_view(client):
authenticate(client)
response = client.post('/change', data={
'password': 'password',
'new_password': 'newpassword',
'new_password_confirm': 'newpassword'
}, follow_redirects=True)
assert b'Profile Page' in response.data
+292
View File
@@ -0,0 +1,292 @@
# -*- coding: utf-8 -*-
"""
test_common
~~~~~~~~~~~
Test common functionality
"""
import base64
try:
from cookielib import Cookie
except ImportError:
from http.cookiejar import Cookie
from utils import authenticate, json_authenticate, logout
def test_login_view(client):
response = client.get('/login')
assert b'<h1>Login</h1>' in response.data
def test_authenticate(client):
response = authenticate(client)
assert response.status_code == 302
response = authenticate(client, follow_redirects=True)
assert b'Hello matt@lp.com' in response.data
def test_authenticate_with_next(client):
data = dict(email='matt@lp.com', password='password')
response = client.post('/login?next=/page1', data=data, follow_redirects=True)
assert b'Page 1' in response.data
def test_authenticate_with_invalid_next(client, get_message):
data = dict(email='matt@lp.com', password='password')
response = client.post('/login?next=http://google.com', data=data)
assert get_message('INVALID_REDIRECT') in response.data
def test_authenticate_with_invalid_malformed_next(client, get_message):
data = dict(email='matt@lp.com', password='password')
response = client.post('/login?next=http:///google.com', data=data)
assert get_message('INVALID_REDIRECT') in response.data
def test_authenticate_case_insensitive_email(app, client):
response = authenticate(client, 'MATT@lp.com', follow_redirects=True)
assert b'Hello matt@lp.com' in response.data
def test_unprovided_username(client, get_message):
response = authenticate(client, "")
assert get_message('EMAIL_NOT_PROVIDED') in response.data
def test_unprovided_password(client, get_message):
response = authenticate(client, password="")
assert get_message('PASSWORD_NOT_PROVIDED') in response.data
def test_invalid_user(client, get_message):
response = authenticate(client, email="bogus@bogus.com")
assert get_message('USER_DOES_NOT_EXIST') in response.data
def test_bad_password(client, get_message):
response = authenticate(client, password="bogus")
assert get_message('INVALID_PASSWORD') in response.data
def test_inactive_user(client, get_message):
response = authenticate(client, "tiya@lp.com", "password")
assert get_message('DISABLED_ACCOUNT') in response.data
def test_unset_password(client, get_message):
response = authenticate(client, "jess@lp.com", "password")
assert get_message('PASSWORD_NOT_SET') in response.data
def test_logout(client):
authenticate(client)
response = logout(client, follow_redirects=True)
assert b'Home Page' in response.data
def test_missing_session_access(client, get_message):
response = client.get('/profile', follow_redirects=True)
assert get_message('LOGIN') in response.data
def test_has_session_access(client):
authenticate(client)
response = client.get("/profile", follow_redirects=True)
assert b'profile' in response.data
def test_authorized_access(client):
authenticate(client)
response = client.get("/admin")
assert b'Admin Page' in response.data
def test_unauthorized_access(client, get_message):
authenticate(client, "joe@lp.com")
response = client.get("/admin", follow_redirects=True)
assert get_message('UNAUTHORIZED') in response.data
def test_roles_accepted(client):
for user in ("matt@lp.com", "joe@lp.com"):
authenticate(client, user)
response = client.get("/admin_or_editor")
assert b'Admin or Editor Page' in response.data
logout(client)
authenticate(client, "jill@lp.com")
response = client.get("/admin_or_editor", follow_redirects=True)
assert b'Home Page' in response.data
def test_unauthenticated_role_required(client, get_message):
response = client.get('/admin', follow_redirects=True)
assert get_message('UNAUTHORIZED') in response.data
def test_multiple_role_required(client):
for user in ("matt@lp.com", "joe@lp.com"):
authenticate(client, user)
response = client.get("/admin_and_editor", follow_redirects=True)
assert b'Home Page' in response.data
client.get('/logout')
authenticate(client, 'dave@lp.com')
response = client.get("/admin_and_editor", follow_redirects=True)
assert b'Admin and Editor Page' in response.data
def test_ok_json_auth(client):
response = json_authenticate(client)
assert response.jdata['meta']['code'] == 200
assert 'authentication_token' in response.jdata['response']['user']
def test_invalid_json_auth(client):
response = json_authenticate(client, password='junk')
assert b'"code": 400' in response.data
def test_token_auth_via_querystring_valid_token(client):
response = json_authenticate(client)
token = response.jdata['response']['user']['authentication_token']
response = client.get('/token?auth_token=' + token)
assert b'Token Authentication' in response.data
def test_token_auth_via_header_valid_token(client):
response = json_authenticate(client)
token = response.jdata['response']['user']['authentication_token']
headers = {"Authentication-Token": token}
response = client.get('/token', headers=headers)
assert b'Token Authentication' in response.data
def test_token_auth_via_querystring_invalid_token(client):
response = client.get('/token?auth_token=X')
assert 401 == response.status_code
def test_token_auth_via_header_invalid_token(client):
response = client.get('/token', headers={"Authentication-Token": 'X'})
assert 401 == response.status_code
def test_http_auth(client):
response = client.get('/http', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:password").decode('utf-8')
})
assert b'HTTP Authentication' in response.data
def test_http_auth_no_authorization(client):
response = client.get('/http', headers={})
assert b'<h1>Unauthorized</h1>' in response.data
assert 'WWW-Authenticate' in response.headers
assert 'Basic realm="Login Required"' == response.headers['WWW-Authenticate']
def test_invalid_http_auth_invalid_username(client):
response = client.get('/http', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"bogus:bogus").decode('utf-8')
})
assert b'<h1>Unauthorized</h1>' in response.data
assert 'WWW-Authenticate' in response.headers
assert 'Basic realm="Login Required"' == response.headers['WWW-Authenticate']
def test_invalid_http_auth_bad_password(client):
response = client.get('/http', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus").decode('utf-8')
})
assert b'<h1>Unauthorized</h1>' in response.data
assert 'WWW-Authenticate' in response.headers
assert 'Basic realm="Login Required"' == response.headers['WWW-Authenticate']
def test_custom_http_auth_realm(client):
response = client.get('/http_custom_realm', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus").decode('utf-8')
})
assert b'<h1>Unauthorized</h1>' in response.data
assert 'WWW-Authenticate' in response.headers
assert 'Basic realm="My Realm"' == response.headers['WWW-Authenticate']
def test_multi_auth_basic(client):
response = client.get('/multi_auth', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:password").decode('utf-8')
})
assert b'Basic' in response.data
response = client.get('/multi_auth')
assert response.status_code == 401
def test_multi_auth_basic_invalid(client):
response = client.get('/multi_auth', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"bogus:bogus").decode('utf-8')
})
assert b'<h1>Unauthorized</h1>' in response.data
assert 'WWW-Authenticate' in response.headers
assert 'Basic realm="Login Required"' == response.headers['WWW-Authenticate']
response = client.get('/multi_auth')
print(response.headers)
assert response.status_code == 401
def test_multi_auth_token(client):
response = json_authenticate(client)
token = response.jdata['response']['user']['authentication_token']
response = client.get('/multi_auth?auth_token=' + token)
assert b'Token' in response.data
def test_multi_auth_session(client):
authenticate(client, )
response = client.get('/multi_auth')
assert b'Session' in response.data
def test_user_deleted_during_session_reverts_to_anonymous_user(app, client):
authenticate(client)
with app.test_request_context('/'):
user = app.security.datastore.find_user(email='matt@lp.com')
app.security.datastore.delete_user(user)
app.security.datastore.commit()
response = client.get('/')
assert b'Hello matt@lp.com' not in response.data
def test_remember_token(client):
response = authenticate(client, follow_redirects=False)
client.cookie_jar.clear_session_cookies()
response = client.get('/profile')
assert b'profile' in response.data
def test_token_loader_does_not_fail_with_invalid_token(client):
c = Cookie(version=0, name='remember_token', value='None', port=None,
port_specified=False, domain='www.example.com',
domain_specified=False, domain_initial_dot=False, path='/',
path_specified=True, secure=False, expires=None,
discard=True, comment=None, comment_url=None,
rest={'HttpOnly': None}, rfc2109=False)
client.cookie_jar.set_cookie(c)
response = client.get('/')
assert b'BadSignature' not in response.data
def test_sending_auth_token_with_json(client):
response = json_authenticate(client)
token = response.jdata['response']['user']['authentication_token']
data = '{"auth_token": "%s"}' % token
response = client.post('/token', data=data, headers={'Content-Type': 'application/json'})
assert b'Token Authentication' in response.data
+45
View File
@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
"""
test_configuration
~~~~~~~~~~~~~~~~~~
Basic configuration tests
"""
import base64
import pytest
from utils import authenticate, logout
@pytest.mark.settings(
logout_url='/custom_logout',
login_url='/custom_login',
post_login_view='/post_login',
post_logout_view='/post_logout',
default_http_auth_realm='Custom Realm')
def test_view_configuration(client):
response = client.get('/custom_login')
assert b"<h1>Login</h1>" in response.data
response = authenticate(client, endpoint='/custom_login')
assert 'location' in response.headers
assert response.headers['Location'] == 'http://localhost/post_login'
response = logout(client, endpoint='/custom_logout')
assert 'location' in response.headers
assert response.headers['Location'] == 'http://localhost/post_logout'
response = client.get('/http', headers={
'Authorization': 'Basic %s' % base64.b64encode(b"joe@lp.com:bogus")
})
assert b'<h1>Unauthorized</h1>' in response.data
assert 'WWW-Authenticate' in response.headers
assert 'Basic realm="Custom Realm"' == response.headers['WWW-Authenticate']
@pytest.mark.settings(login_user_template='custom_security/login_user.html')
def test_template_configuration(client):
response = client.get('/login')
assert b'CUSTOM LOGIN USER' in response.data
+167
View File
@@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
"""
test_confirmable
~~~~~~~~~~~~~~~~
Confirmable tests
"""
import time
import pytest
from flask import Flask
from flask_security.core import UserMixin
from flask_security.signals import user_confirmed, confirm_instructions_sent
from flask_security.utils import capture_registrations
from utils import authenticate, logout
pytestmark = pytest.mark.confirmable()
@pytest.mark.registerable()
def test_confirmable_flag(app, client, sqlalchemy_datastore, get_message):
recorded_confirms = []
recorded_instructions_sent = []
@user_confirmed.connect_via(app)
def on_confirmed(app, user):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
recorded_confirms.append(user)
@confirm_instructions_sent.connect_via(app)
def on_instructions_sent(app, user):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
recorded_instructions_sent.append(user)
# Test login before confirmation
email = 'dude@lp.com'
with capture_registrations() as registrations:
data = dict(email=email, password='password', next='')
response = client.post('/register', data=data)
assert response.status_code == 302
response = authenticate(client, email=email)
assert get_message('CONFIRMATION_REQUIRED') in response.data
# Test invalid token
response = client.get('/confirm/bogus', follow_redirects=True)
assert get_message('INVALID_CONFIRMATION_TOKEN') in response.data
# Test JSON
response = client.post('/confirm', data='{"email": "matt@lp.com"}', headers={
'Content-Type': 'application/json'
})
assert response.status_code == 200
assert response.headers['Content-Type'] == 'application/json'
assert 'user' in response.jdata['response']
assert len(recorded_instructions_sent) == 1
# Test ask for instructions with invalid email
response = client.post('/confirm', data=dict(email='bogus@bogus.com'))
assert get_message('USER_DOES_NOT_EXIST') in response.data
# Test resend instructions
response = client.post('/confirm', data=dict(email=email))
assert get_message('CONFIRMATION_REQUEST', email=email) in response.data
assert len(recorded_instructions_sent) == 2
# Test confirm
token = registrations[0]['confirm_token']
response = client.get('/confirm/' + token, follow_redirects=True)
assert get_message('EMAIL_CONFIRMED') in response.data
assert len(recorded_confirms) == 1
# Test already confirmed
response = client.get('/confirm/' + token, follow_redirects=True)
assert get_message('ALREADY_CONFIRMED') in response.data
# Test already confirmed when asking for confirmation instructions
logout(client)
response = client.get('/confirm')
assert response.status_code == 200
response = client.post('/confirm', data=dict(email=email))
assert get_message('ALREADY_CONFIRMED') in response.data
# Test user was deleted before confirmation
with capture_registrations() as registrations:
data = dict(email='mary@lp.com', password='password', next='')
client.post('/register', data=data)
user = registrations[0]['user']
token = registrations[0]['confirm_token']
with app.app_context():
sqlalchemy_datastore.delete(user)
sqlalchemy_datastore.commit()
response = client.get('/confirm/' + token, follow_redirects=True)
assert get_message('INVALID_CONFIRMATION_TOKEN') in response.data
@pytest.mark.registerable()
@pytest.mark.settings(confirm_email_within='1 milliseconds')
def test_expired_confirmation_token(client, get_message):
with capture_registrations() as registrations:
data = dict(email='mary@lp.com', password='password', next='')
client.post('/register', data=data, follow_redirects=True)
user = registrations[0]['user']
token = registrations[0]['confirm_token']
time.sleep(1)
response = client.get('/confirm/' + token, follow_redirects=True)
msg = get_message('CONFIRMATION_EXPIRED', within='1 milliseconds', email=user.email)
assert msg in response.data
@pytest.mark.registerable()
@pytest.mark.settings(login_without_confirmation=True)
def test_login_when_unconfirmed(client, get_message):
data = dict(email='mary@lp.com', password='password', next='')
response = client.post('/register', data=data, follow_redirects=True)
assert b'mary@lp.com' in response.data
@pytest.mark.registerable()
@pytest.mark.settings(login_without_confirmation=True)
def test_confirmation_different_user_when_logged_in(client, get_message):
e1 = 'dude@lp.com'
e2 = 'lady@lp.com'
with capture_registrations() as registrations:
for e in e1, e2:
data = dict(email=e, password='password', next='')
client.post('/register', data=data)
logout(client)
token1 = registrations[0]['confirm_token']
token2 = registrations[1]['confirm_token']
client.get('/confirm/' + token1, follow_redirects=True)
logout(client)
authenticate(client, email=e1)
response = client.get('/confirm/' + token2, follow_redirects=True)
assert get_message('EMAIL_CONFIRMED') in response.data
assert b'Hello lady@lp.com' in response.data
@pytest.mark.registerable()
@pytest.mark.settings(recoverable=True)
def test_cannot_reset_password_when_email_is_not_confirmed(client, get_message):
email = 'dude@lp.com'
data = dict(email=email, password='password', next='')
response = client.post('/register', data=data, follow_redirects=True)
response = client.post('/reset', data=dict(email=email), follow_redirects=True)
assert get_message('CONFIRMATION_REQUIRED') in response.data
+102
View File
@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
"""
test_context_processors
~~~~~~~~~~~~~~~~~~~~~~~
Context processor tests
"""
import pytest
from utils import authenticate
@pytest.mark.recoverable()
@pytest.mark.registerable()
@pytest.mark.confirmable()
@pytest.mark.changeable()
@pytest.mark.settings(
login_without_confirmation=True,
change_password_template='custom_security/change_password.html',
login_user_template='custom_security/login_user.html',
reset_password_template='custom_security/reset_password.html',
forgot_password_template='custom_security/forgot_password.html',
send_confirmation_template='custom_security/send_confirmation.html',
register_user_template='custom_security/register_user.html')
def test_context_processors(client, app):
@app.security.context_processor
def default_ctx_processor():
return {'global': 'global'}
@app.security.forgot_password_context_processor
def forgot_password():
return {'foo': 'bar'}
response = client.get('/reset')
assert b'global' in response.data
assert b'bar' in response.data
@app.security.login_context_processor
def login():
return {'foo': 'bar'}
response = client.get('/login')
assert b'global' in response.data
assert b'bar' in response.data
@app.security.register_context_processor
def register():
return {'foo': 'bar'}
response = client.get('/register')
assert b'global' in response.data
assert b'bar' in response.data
@app.security.reset_password_context_processor
def reset_password():
return {'foo': 'bar'}
response = client.get('/reset')
assert b'global' in response.data
assert b'bar' in response.data
@app.security.change_password_context_processor
def change_password():
return {'foo': 'bar'}
authenticate(client)
response = client.get('/change')
assert b'global' in response.data
assert b'bar' in response.data
@app.security.send_confirmation_context_processor
def send_confirmation():
return {'foo': 'bar'}
response = client.get('/confirm')
assert b'global' in response.data
assert b'bar' in response.data
@app.security.mail_context_processor
def mail():
return {'foo': 'bar'}
client.get('/logout')
with app.mail.record_messages() as outbox:
client.post('/reset', data=dict(email='matt@lp.com'))
email = outbox[0]
assert 'global' in email.html
assert 'bar' in email.html
@pytest.mark.passwordless()
@pytest.mark.settings(send_login_template='custom_security/send_login.html')
def test_passwordless_login_context_processor(app, client):
@app.security.send_login_context_processor
def send_login():
return {'foo': 'bar'}
response = client.get('/login')
assert b'bar' in response.data
+153
View File
@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
"""
test_datastore
~~~~~~~~~~~~~~
Datastore tests
"""
from pytest import raises
from flask_security import UserMixin, RoleMixin
from flask_security.datastore import Datastore, UserDatastore
from utils import init_app_with_options
class User(UserMixin):
pass
class Role(RoleMixin):
pass
def test_unimplemented_datastore_methods():
datastore = Datastore(None)
assert datastore.db is None
with raises(NotImplementedError):
datastore.put(None)
with raises(NotImplementedError):
datastore.delete(None)
assert not datastore.commit()
def test_unimplemented_user_datastore_methods():
datastore = UserDatastore(None, None)
with raises(NotImplementedError):
datastore.find_user(None)
with raises(NotImplementedError):
datastore.find_role(None)
with raises(NotImplementedError):
datastore.get_user(None)
def test_toggle_active():
datastore = UserDatastore(None, None)
user = User()
user.active = True
assert datastore.toggle_active(user) is True
assert not user.active
assert datastore.toggle_active(user) is True
assert user.active is True
def test_deactivate_user():
datastore = UserDatastore(None, None)
user = User()
user.active = True
assert datastore.deactivate_user(user) is True
assert not user.active
def test_activate_user():
datastore = UserDatastore(None, None)
user = User()
user.active = False
assert datastore.activate_user(user) is True
assert user.active is True
def test_deactivate_returns_false_if_already_false():
datastore = UserDatastore(None, None)
user = User()
user.active = False
assert not datastore.deactivate_user(user)
def test_activate_returns_false_if_already_true():
datastore = UserDatastore(None, None)
user = User()
user.active = True
assert not datastore.activate_user(user)
def test_get_user(app, datastore):
init_app_with_options(app, datastore, **{
'SECURITY_USER_IDENTITY_ATTRIBUTES': ('email', 'username')
})
with app.app_context():
user_id = datastore.find_user(email='matt@lp.com').id
user = datastore.get_user(user_id)
assert user is not None
user = datastore.get_user('matt@lp.com')
assert user is not None
user = datastore.get_user('matt')
assert user is not None
def test_find_role(app, datastore):
init_app_with_options(app, datastore)
role = datastore.find_role('admin')
assert role is not None
role = datastore.find_role('bogus')
assert role is None
def test_add_role_to_user(app, datastore):
init_app_with_options(app, datastore)
# Test with user object
user = datastore.find_user(email='matt@lp.com')
assert user.has_role('editor') is False
assert datastore.add_role_to_user(user, 'editor') is True
assert datastore.add_role_to_user(user, 'editor') is False
assert user.has_role('editor') is True
# Test with email
assert datastore.add_role_to_user('jill@lp.com', 'editor') is True
user = datastore.find_user(email='jill@lp.com')
assert user.has_role('editor') is True
# Test remove role
assert datastore.remove_role_from_user(user, 'editor') is True
assert datastore.remove_role_from_user(user, 'editor') is False
def test_create_user_with_roles(app, datastore):
init_app_with_options(app, datastore)
role = datastore.find_role('admin')
datastore.commit()
user = datastore.create_user(email='dude@lp.com', username='dude',
password='password', roles=[role])
datastore.commit()
user = datastore.find_user(email='dude@lp.com')
assert user.has_role('admin') is True
def test_delete_user(app, datastore):
init_app_with_options(app, datastore)
user = datastore.find_user(email='matt@lp.com')
datastore.delete_user(user)
datastore.commit()
user = datastore.find_user(email='matt@lp.com')
assert user is None
+46
View File
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
"""
test_entities
~~~~~~~~~~~~~
Entity tests
"""
from flask_security import RoleMixin, UserMixin, AnonymousUser
class Role(RoleMixin):
def __init__(self, name):
self.name = name
class User(UserMixin):
def __init__(self, roles):
self.roles = roles
def test_role_mixin_equal():
admin1 = Role('admin')
admin2 = Role('admin')
assert admin1 == admin2
def test_role_mixin_not_equal():
admin = Role('admin')
editor = Role('editor')
assert admin != editor
def test_user_mixin_has_role_with_string():
admin = Role('admin')
editor = Role('editor')
user = User([admin, editor])
assert user.has_role('admin') is True
assert user.has_role('editor') is True
assert user.has_role(admin) is True
assert user.has_role(editor) is True
def test_anonymous_user_has_no_roles():
user = AnonymousUser()
assert not user.has_role('admin')
+38
View File
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
"""
test_hashing
~~~~~~~~~~~~
hashing tests
"""
from pytest import raises
from flask_security.utils import verify_password, encrypt_password
from utils import authenticate, init_app_with_options
def test_verify_password_bcrypt(app, sqlalchemy_datastore):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_PASSWORD_HASH': 'bcrypt',
'SECURITY_PASSWORD_SALT': 'salty'
})
with app.app_context():
assert verify_password('pass', encrypt_password('pass'))
def test_login_with_bcrypt_enabled(app, sqlalchemy_datastore):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_PASSWORD_HASH': 'bcrypt',
'SECURITY_PASSWORD_SALT': 'salty'
})
response = authenticate(app.test_client(), follow_redirects=True)
assert b'Home Page' in response.data
def test_missing_hash_salt_option(app, sqlalchemy_datastore):
with raises(RuntimeError):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_PASSWORD_HASH': 'bcrypt',
})
+210
View File
@@ -0,0 +1,210 @@
# -*- coding: utf-8 -*-
"""
test_emails
~~~~~~~~~~~
Email functionality tests
"""
import pytest
from flask_security import Security
from flask_security.forms import LoginForm, RegisterForm, ConfirmRegisterForm, \
SendConfirmationForm, PasswordlessLoginForm, ForgotPasswordForm, ResetPasswordForm, \
ChangePasswordForm, StringField, PasswordField, email_required, email_validator, \
valid_user_email
from flask_security.utils import capture_reset_password_requests, md5, string_types
from utils import authenticate, init_app_with_options, populate_data
@pytest.mark.recoverable()
def test_async_email_task(app, client):
app.mail_sent = False
@app.security.send_mail_task
def send_email(msg):
app.mail_sent = True
client.post('/reset', data=dict(email='matt@lp.com'))
assert app.mail_sent is True
def test_register_blueprint_flag(app, sqlalchemy_datastore):
app.security = Security(app, datastore=Security, register_blueprint=False)
client = app.test_client()
response = client.get('/login')
assert response.status_code == 404
@pytest.mark.registerable()
@pytest.mark.recoverable()
@pytest.mark.changeable()
def test_basic_custom_forms(app, sqlalchemy_datastore):
class MyLoginForm(LoginForm):
email = StringField('My Login Email Address Field')
class MyRegisterForm(RegisterForm):
email = StringField('My Register Email Address Field')
class MyForgotPasswordForm(ForgotPasswordForm):
email = StringField('My Forgot Email Address Field',
validators=[email_required, email_validator, valid_user_email])
class MyResetPasswordForm(ResetPasswordForm):
password = StringField('My Reset Password Field')
class MyChangePasswordForm(ChangePasswordForm):
password = PasswordField('My Change Password Field')
app.security = Security(app,
datastore=sqlalchemy_datastore,
login_form=MyLoginForm,
register_form=MyRegisterForm,
forgot_password_form=MyForgotPasswordForm,
reset_password_form=MyResetPasswordForm,
change_password_form=MyChangePasswordForm)
populate_data(app)
client = app.test_client()
response = client.get('/login')
assert b'My Login Email Address Field' in response.data
response = client.get('/register')
assert b'My Register Email Address Field' in response.data
response = client.get('/reset')
assert b'My Forgot Email Address Field' in response.data
with capture_reset_password_requests() as requests:
response = client.post('/reset', data=dict(email='matt@lp.com'))
token = requests[0]['token']
response = client.get('/reset/' + token)
assert b'My Reset Password Field' in response.data
authenticate(client)
response = client.get('/change')
assert b'My Change Password Field' in response.data
@pytest.mark.registerable()
@pytest.mark.confirmable()
def test_confirmable_custom_form(app, sqlalchemy_datastore):
app.config['SECURITY_REGISTERABLE'] = True
app.config['SECURITY_CONFIRMABLE'] = True
class MyRegisterForm(ConfirmRegisterForm):
email = StringField('My Register Email Address Field')
class MySendConfirmationForm(SendConfirmationForm):
email = StringField('My Send Confirmation Email Address Field')
app.security = Security(app,
datastore=sqlalchemy_datastore,
send_confirmation_form=MySendConfirmationForm,
confirm_register_form=MyRegisterForm)
client = app.test_client()
response = client.get('/register')
assert b'My Register Email Address Field' in response.data
response = client.get('/confirm')
assert b'My Send Confirmation Email Address Field' in response.data
def test_passwordless_custom_form(app, sqlalchemy_datastore):
app.config['SECURITY_PASSWORDLESS'] = True
class MyPasswordlessLoginForm(PasswordlessLoginForm):
email = StringField('My Passwordless Email Address Field')
app.security = Security(app,
datastore=sqlalchemy_datastore,
passwordless_login_form=MyPasswordlessLoginForm)
client = app.test_client()
response = client.get('/login')
assert b'My Passwordless Email Address Field' in response.data
def test_addition_identity_attributes(app, sqlalchemy_datastore):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_USER_IDENTITY_ATTRIBUTES': ('email', 'username')
})
client = app.test_client()
response = authenticate(client, email='matt', follow_redirects=True)
assert b'Hello matt@lp.com' in response.data
def test_flash_messages_off(app, sqlalchemy_datastore, get_message):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_FLASH_MESSAGES': False
})
client = app.test_client()
response = client.get('/profile')
assert get_message('LOGIN') not in response.data
def test_invalid_hash_scheme(app, sqlalchemy_datastore, get_message):
with pytest.raises(ValueError):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_PASSWORD_HASH': 'bogus'
})
def test_change_hash_type(app, sqlalchemy_datastore):
init_app_with_options(app, sqlalchemy_datastore, **{
'SECURITY_PASSWORD_SCHEMES': ['bcrypt', 'plaintext']
})
app.config['SECURITY_PASSWORD_HASH'] = 'bcrypt'
app.config['SECURITY_PASSWORD_SALT'] = 'salty'
app.security = Security(app, datastore=sqlalchemy_datastore, register_blueprint=False)
client = app.test_client()
response = client.post('/login', data=dict(email='matt@lp.com', password='password'))
assert response.status_code == 302
response = client.get('/logout')
response = client.post('/login', data=dict(email='matt@lp.com', password='password'))
assert response.status_code == 302
def test_md5():
data = md5(b'hello')
assert isinstance(data, string_types)
data = md5(u'hellö')
assert isinstance(data, string_types)
@pytest.mark.settings(password_salt=u'öööööööööööööööööööööööööööööööööö',
password_hash='bcrypt')
def test_password_unicode_password_salt(client):
response = authenticate(client)
assert response.status_code == 302
response = authenticate(client, follow_redirects=True)
assert b'Hello matt@lp.com' in response.data
def test_set_unauthorized_handler(app, client):
@app.security.unauthorized_handler
def unauthorized():
app.unauthorized_handler_set = True
return 'unauthorized-handler-set', 401
app.unauthorized_handler_set = False
authenticate(client, "joe@lp.com")
response = client.get("/admin", follow_redirects=True)
assert app.unauthorized_handler_set is True
assert b'unauthorized-handler-set' in response.data
assert response.status_code == 401
+94
View File
@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
"""
test_passwordless
~~~~~~~~~~~~~~~~~
Passwordless tests
"""
import time
import pytest
from flask import Flask
from flask_security.core import UserMixin
from flask_security.signals import login_instructions_sent
from flask_security.utils import capture_passwordless_login_requests, string_types
from utils import logout
pytestmark = pytest.mark.passwordless()
def test_trackable_flag(app, client, get_message):
recorded = []
@login_instructions_sent.connect_via(app)
def on_instructions_sent(app, user, login_token):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
assert isinstance(login_token, string_types)
recorded.append(user)
# Test disabled account
response = client.post('/login', data=dict(email='tiya@lp.com'), follow_redirects=True)
assert get_message('DISABLED_ACCOUNT') in response.data
# Test login with json and valid email
data = '{"email": "matt@lp.com", "password": "password"}'
response = client.post('/login', data=data, headers={'Content-Type': 'application/json'})
assert response.status_code == 200
assert len(recorded) == 1
# Test login with json and invalid email
data = '{"email": "nobody@lp.com", "password": "password"}'
response = client.post('/login', data=data, headers={'Content-Type': 'application/json'})
assert b'errors' in response.data
# Test sends email and shows appropriate response
with capture_passwordless_login_requests() as requests:
with app.mail.record_messages() as outbox:
response = client.post('/login', data=dict(email='matt@lp.com'), follow_redirects=True)
assert len(recorded) == 2
assert len(requests) == 1
assert len(outbox) == 1
assert 'user' in requests[0]
assert 'login_token' in requests[0]
user = requests[0]['user']
assert get_message('LOGIN_EMAIL_SENT', email=user.email) in response.data
token = requests[0]['login_token']
response = client.get('/login/' + token, follow_redirects=True)
assert get_message('PASSWORDLESS_LOGIN_SUCCESSFUL') in response.data
# Test already authenticated
response = client.get('/login/' + token, follow_redirects=True)
assert get_message('PASSWORDLESS_LOGIN_SUCCESSFUL') not in response.data
logout(client)
# Test invalid token
response = client.get('/login/bogus', follow_redirects=True)
assert get_message('INVALID_LOGIN_TOKEN') in response.data
# Test login request with invalid email
response = client.post('/login', data=dict(email='bogus@bogus.com'))
assert get_message('USER_DOES_NOT_EXIST') in response.data
@pytest.mark.settings(login_within='1 milliseconds')
def test_expired_login_token(client, app, get_message):
e = 'matt@lp.com'
with capture_passwordless_login_requests() as requests:
client.post('/login', data=dict(email=e), follow_redirects=True)
token = requests[0]['login_token']
user = requests[0]['user']
time.sleep(1)
response = client.get('/login/' + token, follow_redirects=True)
assert get_message('LOGIN_EXPIRED', within='1 milliseconds', email=user.email) in response.data
+183
View File
@@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
"""
test_recoverable
~~~~~~~~~~~~~~~~
Recoverable functionality tests
"""
import time
import pytest
from flask import Flask
from flask_security.core import UserMixin
from flask_security.signals import reset_password_instructions_sent, password_reset
from flask_security.utils import capture_reset_password_requests, string_types
from utils import authenticate, logout
pytestmark = pytest.mark.recoverable()
def test_recoverable_flag(app, client, get_message):
recorded_resets = []
recorded_instructions_sent = []
@password_reset.connect_via(app)
def on_password_reset(app, user):
recorded_resets.append(user)
@reset_password_instructions_sent.connect_via(app)
def on_instructions_sent(app, user, token):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
assert isinstance(token, string_types)
recorded_instructions_sent.append(user)
# Test the reset view
response = client.get('/reset')
assert b'<h1>Send password reset instructions</h1>' in response.data
# Test submitting email to reset password creates a token and sends email
with capture_reset_password_requests() as requests:
with app.mail.record_messages() as outbox:
response = client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True)
assert len(recorded_instructions_sent) == 1
assert len(outbox) == 1
assert response.status_code == 200
assert get_message('PASSWORD_RESET_REQUEST', email='joe@lp.com') in response.data
token = requests[0]['token']
# Test view for reset token
response = client.get('/reset/' + token)
assert b'<h1>Reset password</h1>' in response.data
# Test submitting a new password
response = client.post('/reset/' + token, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('PASSWORD_RESET') in response.data
assert len(recorded_resets) == 1
logout(client)
# Test logging in with the new password
response = authenticate(client, 'joe@lp.com', 'newpassword', follow_redirects=True)
assert b'Hello joe@lp.com' in response.data
logout(client)
# Test submitting JSON
response = client.post('/reset', data='{"email": "joe@lp.com"}', headers={
'Content-Type': 'application/json'
})
assert response.headers['Content-Type'] == 'application/json'
assert 'user' not in response.jdata['response']
logout(client)
# Test invalid email
response = client.post('/reset', data=dict(email='bogus@lp.com'), follow_redirects=True)
assert get_message('USER_DOES_NOT_EXIST') in response.data
logout(client)
# Test invalid token
response = client.post('/reset/bogus', data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('INVALID_RESET_PASSWORD_TOKEN') in response.data
# Test mangled token
token = ("WyIxNjQ2MzYiLCIxMzQ1YzBlZmVhM2VhZjYwODgwMDhhZGU2YzU0MzZjMiJd.BZEw_Q.lQyo3npdPZtcJ"
"_sNHVHP103syjM&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d")
response = client.post('/reset/' + token, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('INVALID_RESET_PASSWORD_TOKEN') in response.data
@pytest.mark.settings(reset_password_within='1 milliseconds')
def test_expired_reset_token(client, get_message):
with capture_reset_password_requests() as requests:
client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True)
user = requests[0]['user']
token = requests[0]['token']
time.sleep(1)
response = client.post('/reset/' + token, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
msg = get_message('PASSWORD_RESET_EXPIRED', within='1 milliseconds', email=user.email)
assert msg in response.data
def test_used_reset_token(client, get_message):
with capture_reset_password_requests() as requests:
client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True)
token = requests[0]['token']
# use the token
response = client.post('/reset/' + token, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('PASSWORD_RESET') in response.data
logout(client)
# attempt to use it a second time
response2 = client.post('/reset/' + token, data={
'password': 'otherpassword',
'password_confirm': 'otherpassword'
}, follow_redirects=True)
msg = get_message('INVALID_RESET_PASSWORD_TOKEN')
assert msg in response2.data
def test_reset_passwordless_user(client, get_message):
with capture_reset_password_requests() as requests:
client.post('/reset', data=dict(email='jess@lp.com'), follow_redirects=True)
token = requests[0]['token']
# use the token
response = client.post('/reset/' + token, data={
'password': 'newpassword',
'password_confirm': 'newpassword'
}, follow_redirects=True)
assert get_message('PASSWORD_RESET') in response.data
@pytest.mark.settings(reset_url='/custom_reset')
def test_custom_reset_url(client):
response = client.get('/custom_reset')
assert response.status_code == 200
@pytest.mark.settings(reset_password_template='custom_security/reset_password.html',
forgot_password_template='custom_security/forgot_password.html')
def test_custom_reset_templates(client):
response = client.get('/reset')
assert b'CUSTOM FORGOT PASSWORD' in response.data
with capture_reset_password_requests() as requests:
client.post('/reset', data=dict(email='joe@lp.com'), follow_redirects=True)
token = requests[0]['token']
response = client.get('/reset/' + token)
assert b'CUSTOM RESET PASSWORD' in response.data
+125
View File
@@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
"""
test_registerable
~~~~~~~~~~~~~~~~~
Registerable tests
"""
import pytest
from flask import Flask
from flask_security.core import UserMixin
from flask_security.signals import user_registered
from utils import authenticate, logout
pytestmark = pytest.mark.registerable()
@pytest.mark.settings(post_register_view='/post_register')
def test_registerable_flag(client, app, get_message):
recorded = []
# Test the register view
response = client.get('/register')
assert b"<h1>Register</h1>" in response.data
# Test registering is successful, sends email, and fires signal
@user_registered.connect_via(app)
def on_user_registerd(app, user, confirm_token):
assert isinstance(app, Flask)
assert isinstance(user, UserMixin)
assert confirm_token is None
recorded.append(user)
data = dict(
email='dude@lp.com', password='password', password_confirm='password',
next=''
)
with app.mail.record_messages() as outbox:
response = client.post('/register', data=data, follow_redirects=True)
assert len(recorded) == 1
assert len(outbox) == 1
assert b'Post Register' in response.data
logout(client)
# Test user can login after registering
response = authenticate(client, email='dude@lp.com', password='password')
assert response.status_code == 302
logout(client)
# Test registering with an existing email
data = dict(
email='dude@lp.com', password='password', password_confirm='password',
next=''
)
response = client.post('/register', data=data, follow_redirects=True)
assert get_message('EMAIL_ALREADY_ASSOCIATED', email='dude@lp.com') in response.data
# Test registering with an existing email but case insensitive
data = dict(
email='Dude@lp.com', password='password', password_confirm='password',
next=''
)
response = client.post('/register', data=data, follow_redirects=True)
assert get_message('EMAIL_ALREADY_ASSOCIATED', email='Dude@lp.com') in response.data
# Test registering with JSON
data = '{ "email": "dude2@lp.com", "password": "password"}'
response = client.post('/register', data=data, headers={'Content-Type': 'application/json'})
assert response.headers['content-type'] == 'application/json'
assert response.jdata['meta']['code'] == 200
logout(client)
# Test registering with invalid JSON
data = '{ "email": "bogus", "password": "password"}'
response = client.post('/register', data=data, headers={'Content-Type': 'application/json'})
assert response.headers['content-type'] == 'application/json'
assert response.jdata['meta']['code'] == 400
logout(client)
# Test ?next param
data = dict(email='dude3@lp.com',
password='password',
password_confirm='password',
next='')
response = client.post('/register?next=/page1', data=data, follow_redirects=True)
assert b'Page 1' in response.data
@pytest.mark.settings(register_url='/custom_register', post_register_view='/post_register')
def test_custom_register_url(client):
response = client.get('/custom_register')
assert b"<h1>Register</h1>" in response.data
data = dict(email='dude@lp.com',
password='password',
password_confirm='password',
next='')
response = client.post('/custom_register', data=data, follow_redirects=True)
assert b'Post Register' in response.data
@pytest.mark.settings(register_user_template='custom_security/register_user.html')
def test_custom_register_tempalate(client):
response = client.get('/register')
assert b'CUSTOM REGISTER USER' in response.data
@pytest.mark.settings(send_register_email=False)
def test_disable_register_emails(client, app):
data = dict(
email='dude@lp.com', password='password', password_confirm='password',
next=''
)
with app.mail.record_messages() as outbox:
client.post('/register', data=data, follow_redirects=True)
assert len(outbox) == 0
+44
View File
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""
test_trackable
~~~~~~~~~~~~~~
Trackable tests
"""
import pytest
from utils import authenticate, logout
pytestmark = pytest.mark.trackable()
def test_trackable_flag(app, client):
e = 'matt@lp.com'
authenticate(client, email=e)
logout(client)
authenticate(client, email=e, headers={'X-Forwarded-For': '127.0.0.1'})
with app.app_context():
user = app.security.datastore.find_user(email=e)
assert user.last_login_at is not None
assert user.current_login_at is not None
assert user.last_login_ip == 'untrackable'
assert user.current_login_ip == '127.0.0.1'
assert user.login_count == 2
def test_trackable_with_multiple_ips_in_headers(app, client):
e = 'matt@lp.com'
authenticate(client, email=e)
logout(client)
authenticate(client, email=e, headers={
'X-Forwarded-For': '99.99.99.99, 88.88.88.88'})
with app.app_context():
user = app.security.datastore.find_user(email=e)
assert user.last_login_at is not None
assert user.current_login_at is not None
assert user.last_login_ip == 'untrackable'
assert user.current_login_ip == '88.88.88.88'
assert user.login_count == 2
-88
View File
@@ -1,88 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
from flask_security import RoleMixin, UserMixin, AnonymousUser
from flask_security.datastore import Datastore, UserDatastore
class Role(RoleMixin):
def __init__(self, name):
self.name = name
class User(UserMixin):
def __init__(self, email, roles):
self.email = email
self.roles = roles
admin = Role('admin')
admin2 = Role('admin')
editor = Role('editor')
user = User('matt@lp.com', [admin, editor])
class SecurityEntityTests(unittest.TestCase):
def test_role_mixin_equal(self):
self.assertEqual(admin, admin2)
def test_role_mixin_not_equal(self):
self.assertNotEqual(admin, editor)
def test_user_mixin_has_role_with_string(self):
self.assertTrue(user.has_role('admin'))
def test_user_mixin_has_role_with_role_obj(self):
self.assertTrue(user.has_role(Role('admin')))
def test_anonymous_user_has_no_roles(self):
au = AnonymousUser()
self.assertEqual(0, len(au.roles))
self.assertFalse(au.has_role('admin'))
class DatastoreTests(unittest.TestCase):
def setUp(self):
super(DatastoreTests, self).setUp()
self.ds = UserDatastore(None, None)
def test_unimplemented_datastore_methods(self):
ds = Datastore(None)
self.assertRaises(NotImplementedError, ds.put, None)
self.assertRaises(NotImplementedError, ds.delete, None)
def test_unimplemented_user_datastore_methods(self):
self.assertRaises(NotImplementedError, self.ds.find_user, None)
self.assertRaises(NotImplementedError, self.ds.find_role, None)
def test_toggle_active(self):
user.active = True
rv = self.ds.toggle_active(user)
self.assertTrue(rv)
self.assertFalse(user.active)
rv = self.ds.toggle_active(user)
self.assertTrue(rv)
self.assertTrue(user.active)
def test_deactivate_user(self):
user.active = True
rv = self.ds.deactivate_user(user)
self.assertTrue(rv)
self.assertFalse(user.active)
def test_activate_user(self):
ds = UserDatastore(None, None)
user.active = False
ds.activate_user(user)
self.assertTrue(user.active)
def test_deactivate_returns_false_if_already_false(self):
user.active = False
self.assertFalse(self.ds.deactivate_user(user))
def test_activate_returns_false_if_already_true(self):
user.active = True
self.assertFalse(self.ds.activate_user(user))
+84
View File
@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
"""
utils
~~~~~
Test utils
"""
from flask import Response as BaseResponse, json
from flask_security import Security
from flask_security.utils import encrypt_password
_missing = object
def authenticate(client, email="matt@lp.com", password="password", endpoint=None, **kwargs):
data = dict(email=email, password=password, remember='y')
return client.post(endpoint or '/login', data=data, **kwargs)
def json_authenticate(client, email="matt@lp.com", password="password", endpoint=None):
data = '{"email": "%s", "password": "%s"}' % (email, password)
return client.post(endpoint or '/login', content_type="application/json", data=data)
def logout(client, endpoint=None, **kwargs):
return client.get(endpoint or '/logout', **kwargs)
def create_roles(ds):
for role in ('admin', 'editor', 'author'):
ds.create_role(name=role)
ds.commit()
def create_users(ds, count=None):
users = [('matt@lp.com', 'matt', 'password', ['admin'], True),
('joe@lp.com', 'joe', 'password', ['editor'], True),
('dave@lp.com', 'dave', 'password', ['admin', 'editor'], True),
('jill@lp.com', 'jill', 'password', ['author'], True),
('tiya@lp.com', 'tiya', 'password', [], False),
('jess@lp.com', 'jess', None, [], True)]
count = count or len(users)
for u in users[:count]:
pw = u[2]
if pw is not None:
pw = encrypt_password(pw)
roles = [ds.find_or_create_role(rn) for rn in u[3]]
ds.commit()
user = ds.create_user(email=u[0], username=u[1], password=pw, active=u[4])
ds.commit()
for role in roles:
ds.add_role_to_user(user, role)
ds.commit()
def populate_data(app, user_count=None):
ds = app.security.datastore
with app.app_context():
create_roles(ds)
create_users(ds, user_count)
class Response(BaseResponse): # pragma: no cover
@property
def jdata(self):
rv = getattr(self, '_cached_jdata', _missing)
if rv is not _missing:
return rv
try:
self._cached_jdata = json.loads(self.data)
except ValueError:
raise Exception('Invalid JSON response')
return self._cached_jdata
def init_app_with_options(app, datastore, **options):
security_args = options.pop('security_args', {})
app.config.update(**options)
app.security = Security(app, datastore=datastore, **security_args)
populate_data(app)
+5 -8
View File
@@ -1,13 +1,10 @@
[tox] [tox]
envlist = py26, py27, py33, pypy envlist = py26, py27, py33, py34, py35, pypy
[testenv] [testenv]
deps = deps =
nose -r{toxinidir}/requirements.txt
simplejson -r{toxinidir}/requirements-dev.txt
Flask-SQLAlchemy
Flask-MongoEngine
Flask-Peewee
bcrypt
commands = nosetests -xs [] commands =
python setup.py test