diff --git a/.travis.yml b/.travis.yml
index 4112361..42c3b47 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,8 +6,8 @@ python:
install:
- pip install . --quiet --use-mirrors
- - "if [[ $TRAVIS_PYTHON_VERSION != '2.7' ]]; then pip install importlib simplejson --quiet --use-mirrors; fi"
- - pip install nose Flask-SQLAlchemy Flask-MongoEngine Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors
+ - "if [[ $TRAVIS_PYTHON_VERSION != '2.7' ]]; then pip install importlib --quiet --use-mirrors; fi"
+ - pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Mail py-bcrypt MySQL-python --quiet --use-mirrors
before_script:
- mysql -e 'create database flask_security_test;'
diff --git a/artwork/logo-helmet.svg b/artwork/logo-helmet.svg
new file mode 100644
index 0000000..2799f1f
--- /dev/null
+++ b/artwork/logo-helmet.svg
@@ -0,0 +1,297 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/_static/logo-full.png b/docs/_static/logo-full.png
new file mode 100644
index 0000000..724fd06
Binary files /dev/null and b/docs/_static/logo-full.png differ
diff --git a/docs/_static/logo-helmet.png b/docs/_static/logo-helmet.png
new file mode 100644
index 0000000..b490418
Binary files /dev/null and b/docs/_static/logo-helmet.png differ
diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html
new file mode 100644
index 0000000..f65d1c7
--- /dev/null
+++ b/docs/_templates/sidebarintro.html
@@ -0,0 +1,17 @@
+
About
+
+ Flask-Security is an opinionated Flask extension which adds basic
+ security and authentication features to your Flask apps quickly
+ and easily. Flask-Social can also be used to add "social" or OAuth
+ login and connection management.
+
+Useful Links
+
+
\ No newline at end of file
diff --git a/docs/_templates/sidebarlogo.html b/docs/_templates/sidebarlogo.html
new file mode 100644
index 0000000..2686677
--- /dev/null
+++ b/docs/_templates/sidebarlogo.html
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/docs/api.rst b/docs/api.rst
index ce4d0d8..efad2ef 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -50,27 +50,6 @@ Datastores
:inherited-members:
-Exceptions
-----------
-.. autoexception:: flask_security.exceptions.BadCredentialsError
-
-.. autoexception:: flask_security.exceptions.AuthenticationError
-
-.. autoexception:: flask_security.exceptions.UserNotFoundError
-
-.. autoexception:: flask_security.exceptions.RoleNotFoundError
-
-.. autoexception:: flask_security.exceptions.UserDatastoreError
-
-.. autoexception:: flask_security.exceptions.UserCreationError
-
-.. autoexception:: flask_security.exceptions.RoleCreationError
-
-.. autoexception:: flask_security.exceptions.ConfirmationError
-
-.. autoexception:: flask_security.exceptions.ResetPasswordError
-
-
Signals
-------
See the documentation for the signals provided by the Flask-Login and
diff --git a/docs/conf.py b/docs/conf.py
index 64f49a5..4432360 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -100,6 +100,8 @@ html_theme = 'flask'
html_theme_options = {
#'github_fork': 'mattupstate/flask-security',
#'index_logo': False
+ 'touch_icon': 'touch-icon.png',
+ 'index_logo': 'logo-full.png'
}
# Add any paths that contain custom themes here, relative to this directory.
@@ -135,7 +137,11 @@ html_static_path = ['_static']
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+html_sidebars = {
+ 'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'],
+ '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html',
+ 'sourcelink.html', 'searchbox.html']
+}
# Additional templates that should be rendered to pages, maps page names to
# template names.
diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc
index 45ff59e..c905e06 100644
--- a/docs/contents.rst.inc
+++ b/docs/contents.rst.inc
@@ -4,7 +4,6 @@ Contents
.. toctree::
:maxdepth: 1
- overview
features
configuration
quickstart
diff --git a/docs/index.rst b/docs/index.rst
index 32784f3..97713c1 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,7 +1,36 @@
Flask-Security
==============
-Flask-Security quickly adds security features to your Flask application.
+Flask-Security allows you to quickly add common security mechanisms to your
+Flask application. They include:
+
+1. Session based authentication
+2. Role management
+3. Password encryption
+4. Basic HTTP authentication
+5. Token based authentication
+6. Token based account activation (optional)
+7. Token based password recovery/resetting (optional)
+8. User registration (optional)
+9. Login tracking (optional)
+
+Many of these features are made possible by integrating various Flask extensions
+and libraries. They include:
+
+1. `Flask-Login `_
+2. `Flask-Mail `_
+3. `Flask-Principal `_
+4. `Flask-Script `_
+5. `Flask-WTF `_
+6. `itsdangerous `_
+7. `passlib `_
+
+Additionally, it assumes you'll be using a common library for your database
+connections and model definitions. Flask-Security supports the following Flask
+extensions out of the box for data persistance:
+
+1. `Flask-SQLAlchemy `_
+2. `Flask-MongoEngine `_
.. include:: contents.rst.inc
\ No newline at end of file
diff --git a/docs/overview.rst b/docs/overview.rst
deleted file mode 100644
index 571a9af..0000000
--- a/docs/overview.rst
+++ /dev/null
@@ -1,33 +0,0 @@
-Overview
-========
-
-Flask-Security allows you to quickly add common security mechanisms to your
-Flask application. They include:
-
-1. Session based authentication
-2. Role management
-3. Password encryption
-4. Basic HTTP authentication
-5. Token based authentication
-6. Token based account activation (optional)
-7. Token based password recovery/resetting (optional)
-8. User registration (optional)
-9. Login tracking (optional)
-
-Many of these features are made possible by integrating various Flask extensions
-and libraries. They include:
-
-1. `Flask-Login `_
-2. `Flask-Mail `_
-3. `Flask-Principal `_
-4. `Flask-Script `_
-5. `Flask-WTF `_
-6. `itsdangerous `_
-7. `passlib `_
-
-Additionally, it assumes you'll be using a common library for your database
-connections and model definitions. Flask-Security supports the following Flask
-extensions out of the box for data persistance:
-
-1. `Flask-SQLAlchemy `_
-2. `Flask-MongoEngine `_
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 736bfed..4a53949 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -3,4 +3,4 @@ source-dir = docs/
build-dir = docs/_build
[upload_sphinx]
-upload-dir = docs/_build/html
\ No newline at end of file
+upload-dir = docs/_build
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 45fa170..6c423a4 100644
--- a/setup.py
+++ b/setup.py
@@ -36,7 +36,7 @@ setup(
install_requires=[
'Flask>=0.9',
'Flask-Login>=0.1.3',
- 'Flask-Mail>=0.6.1',
+ 'Flask-Mail>=0.7.0',
'Flask-Principal>=0.3',
'Flask-WTF>=0.5.4',
'itsdangerous>=0.15',
@@ -47,7 +47,8 @@ setup(
'nose',
'Flask-SQLAlchemy',
'Flask-MongoEngine',
- 'py-bcrypt'
+ 'py-bcrypt',
+ 'simplejson'
],
classifiers=[
'Development Status :: 4 - Beta',
diff --git a/tests/configured_tests.py b/tests/configured_tests.py
new file mode 100644
index 0000000..0ac9d6f
--- /dev/null
+++ b/tests/configured_tests.py
@@ -0,0 +1,463 @@
+from __future__ import with_statement
+
+import base64
+import time
+import simplejson as json
+
+from flask.ext.security.utils import capture_registrations, \
+ capture_reset_password_requests, capture_passwordless_login_requests
+
+from tests import SecurityTest
+
+
+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('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("Login ", r.data)
+
+ def test_authenticate(self):
+ r = self.authenticate(endpoint="/custom_login")
+ self.assertIn('Post Login', r.data)
+
+ def test_logout(self):
+ self.authenticate(endpoint="/custom_login")
+ r = self.logout(endpoint="/custom_logout")
+ self.assertIn('Post Logout', r.data)
+
+ def test_register_view(self):
+ r = self._get('/register')
+ self.assertIn('Register ', 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('Post Register', r.data)
+
+ def test_register_json(self):
+ r = self._post('/register',
+ data='{ "email": "dude@lp.com", "password": "password" }',
+ content_type='application/json')
+ data = json.loads(r.data)
+ self.assertEquals(data['meta']['code'], 200)
+ self.assertIn('authentication_token', data['response']['user'])
+
+ 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)
+ self.assertIn('matt@lp.com is already associated with an account', r.data)
+
+ def test_unauthorized(self):
+ self.authenticate("joe@lp.com", endpoint="/custom_auth")
+ r = self._get("/admin", follow_redirects=True)
+ msg = '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 ' + base64.b64encode("joe@lp.com:bogus")
+ })
+ self.assertIn('Unauthorized ', 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 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.client.post('/register', data=data, follow_redirects=True)
+ r = self.authenticate('dude@lp.com')
+ self.assertIn('Hello dude@lp.com', r.data)
+
+
+class ConfirmableTests(SecurityTest):
+ AUTH_CONFIG = {
+ 'SECURITY_CONFIRMABLE': True,
+ 'SECURITY_REGISTERABLE': True,
+ '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'), r.data)
+
+ def test_send_confirmation_of_already_confirmed_account(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()
+ r = self.client.post('/confirm', data=dict(email=e))
+ self.assertIn(self.get_message('ALREADY_CONFIRMED'), 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)
+
+ def test_confirm_email(self):
+ e = 'dude@lp.com'
+
+ with capture_registrations() as registrations:
+ self.register(e)
+ token = registrations[0]['confirm_token']
+
+ r = self.client.get('/confirm/' + token, follow_redirects=True)
+
+ msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
+ self.assertIn(msg, r.data)
+
+ def test_invalid_token_when_confirming_email(self):
+ r = self.client.get('/confirm/bogus', follow_redirects=True)
+ self.assertIn('Invalid confirmation token', r.data)
+
+ def test_send_confirmation_with_invalid_email(self):
+ r = self._post('/confirm', data=dict(email='bogus@bogus.com'))
+ self.assertIn('Specified user does not exist', 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)
+ self.assertIn(msg, 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, 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.client.post('/register', data=data, follow_redirects=True)
+ self.assertIn(e, 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.client.post('/reset',
+ data=dict(email='joe@lp.com'),
+ follow_redirects=True)
+ t = requests[0]['token']
+ r = self._get('/reset/' + t)
+ self.assertIn('Reset password ', r.data)
+
+ def test_forgot_post_sends_email(self):
+ with capture_reset_password_requests():
+ with self.app.extensions['mail'].record_messages() as outbox:
+ self.client.post('/reset', data=dict(email='joe@lp.com'))
+ self.assertEqual(len(outbox), 1)
+
+ def test_forgot_password_invalid_email(self):
+ r = self.client.post('/reset',
+ data=dict(email='larry@lp.com'),
+ follow_redirects=True)
+ self.assertIn("Specified user does not exist", r.data)
+
+ def test_reset_password_with_valid_token(self):
+ with capture_reset_password_requests() as requests:
+ r = self.client.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('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)
+
+ self.assertIn(self.get_message('INVALID_RESET_PASSWORD_TOKEN'), 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.client.post('/reset',
+ data=dict(email='joe@lp.com'),
+ follow_redirects=True)
+ t = requests[0]['token']
+
+ time.sleep(1)
+
+ r = self.client.post('/reset/' + t, data={
+ 'password': 'newpassword',
+ 'password_confirm': 'newpassword'
+ }, follow_redirects=True)
+
+ self.assertIn('You did not reset your password within', r.data)
+
+
+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.client.post('/login',
+ data=dict(email='tiya@lp.com'),
+ follow_redirects=True)
+ self.assertIn(msg, 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.client.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, r.data)
+
+ r = self.client.get('/login/' + token, follow_redirects=True)
+ msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
+ self.assertIn(msg, r.data)
+
+ r = self.client.get('/profile')
+ self.assertIn('Profile Page', r.data)
+
+ def test_invalid_login_token(self):
+ msg = self.app.config['SECURITY_MSG_INVALID_LOGIN_TOKEN'][0]
+ r = self._get('/login/bogus', follow_redirects=True)
+ self.assertIn(msg, r.data)
+
+ def test_token_login_when_already_authenticated(self):
+ with capture_passwordless_login_requests() as requests:
+ self.client.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, r.data)
+
+ r = self.client.get('/login/' + token, follow_redirects=True)
+ msg = self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')
+ self.assertNotIn(msg, r.data)
+
+ def test_send_login_with_invalid_email(self):
+ r = self._post('/login', data=dict(email='bogus@bogus.com'))
+ self.assertIn('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.client.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, 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.client.post('/reset', data=dict(email='matt@lp.com'))
+ self.assertTrue(self.mail_sent)
+
+
+class NoBlueprintTests(SecurityTest):
+
+ AUTH_CONFIG = {
+ 'USER_COUNT': 1
+ }
+
+ def _create_app(self, auth_config):
+ return super(NoBlueprintTests, self)._create_app(auth_config, False)
+
+ 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 = 'Basic ' + base64.b64encode("matt@lp.com:password")
+ r = self._get('/http', headers={'Authorization': auth})
+ self.assertIn('HTTP Authentication', r.data)
diff --git a/tests/functional_tests.py b/tests/functional_tests.py
index 8f2a0ba..fa3bb05 100644
--- a/tests/functional_tests.py
+++ b/tests/functional_tests.py
@@ -3,17 +3,9 @@
from __future__ import with_statement
import base64
-import time
-
+import simplejson as json
from cookielib import Cookie
-try:
- import simplejson as json
-except ImportError:
- import json
-
-from flask.ext.security.utils import capture_registrations, \
- capture_reset_password_requests, capture_passwordless_login_requests
from werkzeug.utils import parse_cookie
from tests import SecurityTest
@@ -197,401 +189,6 @@ class DefaultSecurityTests(SecurityTest):
self.assertNotIn('BadSignature', r.data)
-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('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("Login ", r.data)
-
- def test_authenticate(self):
- r = self.authenticate(endpoint="/custom_login")
- self.assertIn('Post Login', r.data)
-
- def test_logout(self):
- self.authenticate(endpoint="/custom_login")
- r = self.logout(endpoint="/custom_logout")
- self.assertIn('Post Logout', r.data)
-
- def test_register_view(self):
- r = self._get('/register')
- self.assertIn('Register ', 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('Post Register', r.data)
-
- def test_register_json(self):
- r = self._post('/register',
- data='{ "email": "dude@lp.com", "password": "password" }',
- content_type='application/json')
- data = json.loads(r.data)
- self.assertEquals(data['meta']['code'], 200)
- self.assertIn('authentication_token', data['response']['user'])
-
- 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)
- self.assertIn('matt@lp.com is already associated with an account', r.data)
-
- def test_unauthorized(self):
- self.authenticate("joe@lp.com", endpoint="/custom_auth")
- r = self._get("/admin", follow_redirects=True)
- msg = '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 ' + base64.b64encode("joe@lp.com:bogus")
- })
- self.assertIn('Unauthorized ', 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 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.client.post('/register', data=data, follow_redirects=True)
- r = self.authenticate('dude@lp.com')
- self.assertIn('Hello dude@lp.com', r.data)
-
-
-class ConfirmableTests(SecurityTest):
- AUTH_CONFIG = {
- 'SECURITY_CONFIRMABLE': True,
- 'SECURITY_REGISTERABLE': True,
- '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'), r.data)
-
- def test_send_confirmation_of_already_confirmed_account(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()
- r = self.client.post('/confirm', data=dict(email=e))
- self.assertIn(self.get_message('ALREADY_CONFIRMED'), 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)
-
- def test_confirm_email(self):
- e = 'dude@lp.com'
-
- with capture_registrations() as registrations:
- self.register(e)
- token = registrations[0]['confirm_token']
-
- r = self.client.get('/confirm/' + token, follow_redirects=True)
-
- msg = self.app.config['SECURITY_MSG_EMAIL_CONFIRMED'][0]
- self.assertIn(msg, r.data)
-
- def test_invalid_token_when_confirming_email(self):
- r = self.client.get('/confirm/bogus', follow_redirects=True)
- self.assertIn('Invalid confirmation token', r.data)
-
- def test_send_confirmation_with_invalid_email(self):
- r = self._post('/confirm', data=dict(email='bogus@bogus.com'))
- self.assertIn('Specified user does not exist', r.data)
-
- def test_resend_confirmation(self):
- e = 'dude@lp.com'
- self.register(e)
- r = self._post('/confirm', data={'email': e})
- self.assertIn(self.get_message('CONFIRMATION_REQUEST', email=e), 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] % dict(within=expire_text, email=e)
- self.assertIn(msg, 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.client.post('/register', data=data, follow_redirects=True)
- self.assertIn(e, 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.client.post('/reset',
- data=dict(email='joe@lp.com'),
- follow_redirects=True)
- t = requests[0]['token']
- r = self._get('/reset/' + t)
- self.assertIn('Reset password ', r.data)
-
- def test_forgot_post_sends_email(self):
- with capture_reset_password_requests():
- with self.app.extensions['mail'].record_messages() as outbox:
- self.client.post('/reset', data=dict(email='joe@lp.com'))
- self.assertEqual(len(outbox), 1)
-
- def test_forgot_password_invalid_email(self):
- r = self.client.post('/reset',
- data=dict(email='larry@lp.com'),
- follow_redirects=True)
- self.assertIn("Specified user does not exist", r.data)
-
- def test_reset_password_with_valid_token(self):
- with capture_reset_password_requests() as requests:
- r = self.client.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('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)
-
- self.assertIn(self.get_message('INVALID_RESET_PASSWORD_TOKEN'), 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.client.post('/reset',
- data=dict(email='joe@lp.com'),
- follow_redirects=True)
- t = requests[0]['token']
-
- time.sleep(1)
-
- r = self.client.post('/reset/' + t, data={
- 'password': 'newpassword',
- 'password_confirm': 'newpassword'
- }, follow_redirects=True)
-
- self.assertIn('You did not reset your password within', r.data)
-
-
-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.client.post('/login', data=dict(email='tiya@lp.com'), follow_redirects=True)
- self.assertIn(msg, 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.client.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] % dict(email=user.email)
- self.assertIn(msg, r.data)
-
- r = self.client.get('/login/' + token, follow_redirects=True)
- self.assertIn(self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'), r.data)
-
- r = self.client.get('/profile')
- self.assertIn('Profile Page', r.data)
-
- def test_invalid_login_token(self):
- msg = self.app.config['SECURITY_MSG_INVALID_LOGIN_TOKEN'][0]
- r = self._get('/login/bogus', follow_redirects=True)
- self.assertIn(msg, r.data)
-
- def test_token_login_forwards_to_post_login_view_when_already_authenticated(self):
- with capture_passwordless_login_requests() as requests:
- self.client.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)
- self.assertIn(self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'), r.data)
-
- r = self.client.get('/login/' + token, follow_redirects=True)
- self.assertNotIn(self.get_message('PASSWORDLESS_LOGIN_SUCCESSFUL'), r.data)
-
- def test_send_login_with_invalid_email(self):
- r = self._post('/login', data=dict(email='bogus@bogus.com'))
- self.assertIn('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.client.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] % dict(within=expire_text, email=e)
- self.assertIn(msg, r.data)
-
- self.assertEqual(len(outbox), 1)
- self.assertIn(e, outbox[0].html)
- self.assertNotIn(token, outbox[0].html)
-
-
class MongoEngineSecurityTests(DefaultSecurityTests):
def _create_app(self, auth_config):
@@ -627,43 +224,3 @@ class MongoEngineDatastoreTests(DefaultDatastoreTests):
def _create_app(self, auth_config):
from tests.test_app.mongoengine import create_app
return create_app(auth_config)
-
-
-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.client.post('/reset', data=dict(email='matt@lp.com'))
- self.assertTrue(self.mail_sent)
-
-
-class NoBlueprintTests(SecurityTest):
-
- AUTH_CONFIG = {
- 'USER_COUNT': 1
- }
-
- def _create_app(self, auth_config):
- return super(NoBlueprintTests, self)._create_app(auth_config, False)
-
- def test_login_endpoint_is_404(self):
- r = self._get('/login')
- self.assertEqual(404, r.status_code)
-
- def test_http_auth_without_blueprint(self):
- r = self._get('/http', headers={
- 'Authorization': 'Basic ' + base64.b64encode("matt@lp.com:password")
- })
- self.assertIn('HTTP Authentication', r.data)