
1) Remove mongo limitations (no 'or' when querying for either username or email). 2) Lost password function revealed if an user name or email address is registered, which can be considered a data leak. Leaking user names is OK, they are public anyway, but don't reveal lookup success in case the lookup happened by email address. Simply respond: "If you have an account here, we have send you your email"? 3) username and email search was case sensitive. Made username search case insensitive (they are always stored lowercase in the db). Keep email-address search case sensitive for now. This might need further discussion 4) Remove a whole bunch of indention in the style of: if no error: ... if no error: ... if no error: actually do something in the regular case by restructuring the function. 5) Outsource the sanity checking for username and email fields into the validator function. This way, we get automatic case sanity checking and sanitizing for all required fields. 6) Require 5-char password and fix tests Originally, the Change password form required a password between 6-30 chars while the registration and login form did not require anything special. This commit introduces a common minimum limit for all forms which breaks the test suite which uses a 5 char password by default. :-). As 5 chars seem sensible enough to enforce (people should be picking much longer ones anyway), just reduce the limit to 5 chars, thereby making all tests pass. Signed-off-by: Sebastian Spaeth <Sebastian@SSpaeth.de>
424 lines
15 KiB
Python
424 lines
15 KiB
Python
# GNU MediaGoblin -- federated, autonomous media hosting
|
|
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import urlparse
|
|
import datetime
|
|
|
|
from nose.tools import assert_equal
|
|
|
|
from mediagoblin import mg_globals
|
|
from mediagoblin.auth import lib as auth_lib
|
|
from mediagoblin.db.models import User
|
|
from mediagoblin.tests.tools import setup_fresh_app, get_app, fixture_add_user
|
|
from mediagoblin.tools import template, mail
|
|
|
|
|
|
########################
|
|
# Test bcrypt auth funcs
|
|
########################
|
|
|
|
def test_bcrypt_check_password():
|
|
# Check known 'lollerskates' password against check function
|
|
assert auth_lib.bcrypt_check_password(
|
|
'lollerskates',
|
|
'$2a$12$PXU03zfrVCujBhVeICTwtOaHTUs5FFwsscvSSTJkqx/2RQ0Lhy/nO')
|
|
|
|
assert not auth_lib.bcrypt_check_password(
|
|
'notthepassword',
|
|
'$2a$12$PXU03zfrVCujBhVeICTwtOaHTUs5FFwsscvSSTJkqx/2RQ0Lhy/nO')
|
|
|
|
|
|
# Same thing, but with extra fake salt.
|
|
assert not auth_lib.bcrypt_check_password(
|
|
'notthepassword',
|
|
'$2a$12$ELVlnw3z1FMu6CEGs/L8XO8vl0BuWSlUHgh0rUrry9DUXGMUNWwl6',
|
|
'3><7R45417')
|
|
|
|
|
|
def test_bcrypt_gen_password_hash():
|
|
pw = 'youwillneverguessthis'
|
|
|
|
# Normal password hash generation, and check on that hash
|
|
hashed_pw = auth_lib.bcrypt_gen_password_hash(pw)
|
|
assert auth_lib.bcrypt_check_password(
|
|
pw, hashed_pw)
|
|
assert not auth_lib.bcrypt_check_password(
|
|
'notthepassword', hashed_pw)
|
|
|
|
|
|
# Same thing, extra salt.
|
|
hashed_pw = auth_lib.bcrypt_gen_password_hash(pw, '3><7R45417')
|
|
assert auth_lib.bcrypt_check_password(
|
|
pw, hashed_pw, '3><7R45417')
|
|
assert not auth_lib.bcrypt_check_password(
|
|
'notthepassword', hashed_pw, '3><7R45417')
|
|
|
|
|
|
@setup_fresh_app
|
|
def test_register_views(test_app):
|
|
"""
|
|
Massive test function that all our registration-related views all work.
|
|
"""
|
|
# Test doing a simple GET on the page
|
|
# -----------------------------------
|
|
|
|
test_app.get('/auth/register/')
|
|
# Make sure it rendered with the appropriate template
|
|
assert template.TEMPLATE_TEST_CONTEXT.has_key(
|
|
'mediagoblin/auth/register.html')
|
|
|
|
# Try to register without providing anything, should error
|
|
# --------------------------------------------------------
|
|
|
|
template.clear_test_template_context()
|
|
test_app.post(
|
|
'/auth/register/', {})
|
|
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
|
|
form = context['register_form']
|
|
assert form.username.errors == [u'This field is required.']
|
|
assert form.password.errors == [u'This field is required.']
|
|
assert form.email.errors == [u'This field is required.']
|
|
|
|
# Try to register with fields that are known to be invalid
|
|
# --------------------------------------------------------
|
|
|
|
## too short
|
|
template.clear_test_template_context()
|
|
test_app.post(
|
|
'/auth/register/', {
|
|
'username': 'l',
|
|
'password': 'o',
|
|
'email': 'l'})
|
|
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
|
|
form = context['register_form']
|
|
|
|
assert_equal (form.username.errors, [u'Field must be between 3 and 30 characters long.'])
|
|
assert_equal (form.password.errors, [u'Field must be between 5 and 1024 characters long.'])
|
|
|
|
## bad form
|
|
template.clear_test_template_context()
|
|
test_app.post(
|
|
'/auth/register/', {
|
|
'username': '@_@',
|
|
'email': 'lollerskates'})
|
|
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/register.html']
|
|
form = context['register_form']
|
|
|
|
assert_equal (form.username.errors, [u'This field does not take email addresses.'])
|
|
assert_equal (form.email.errors, [u'This field requires an email address.'])
|
|
|
|
## At this point there should be no users in the database ;)
|
|
assert_equal(User.query.count(), 0)
|
|
|
|
# Successful register
|
|
# -------------------
|
|
template.clear_test_template_context()
|
|
response = test_app.post(
|
|
'/auth/register/', {
|
|
'username': u'happygirl',
|
|
'password': 'iamsohappy',
|
|
'email': 'happygrrl@example.org'})
|
|
response.follow()
|
|
|
|
## Did we redirect to the proper page? Use the right template?
|
|
assert_equal(
|
|
urlparse.urlsplit(response.location)[2],
|
|
'/u/happygirl/')
|
|
assert template.TEMPLATE_TEST_CONTEXT.has_key(
|
|
'mediagoblin/user_pages/user.html')
|
|
|
|
## Make sure user is in place
|
|
new_user = mg_globals.database.User.find_one(
|
|
{'username': u'happygirl'})
|
|
assert new_user
|
|
assert new_user.status == u'needs_email_verification'
|
|
assert new_user.email_verified == False
|
|
|
|
## Make sure user is logged in
|
|
request = template.TEMPLATE_TEST_CONTEXT[
|
|
'mediagoblin/user_pages/user.html']['request']
|
|
assert request.session['user_id'] == unicode(new_user.id)
|
|
|
|
## Make sure we get email confirmation, and try verifying
|
|
assert len(mail.EMAIL_TEST_INBOX) == 1
|
|
message = mail.EMAIL_TEST_INBOX.pop()
|
|
assert message['To'] == 'happygrrl@example.org'
|
|
email_context = template.TEMPLATE_TEST_CONTEXT[
|
|
'mediagoblin/auth/verification_email.txt']
|
|
assert email_context['verification_url'] in message.get_payload(decode=True)
|
|
|
|
path = urlparse.urlsplit(email_context['verification_url'])[2]
|
|
get_params = urlparse.urlsplit(email_context['verification_url'])[3]
|
|
assert path == u'/auth/verify_email/'
|
|
parsed_get_params = urlparse.parse_qs(get_params)
|
|
|
|
### user should have these same parameters
|
|
assert parsed_get_params['userid'] == [
|
|
unicode(new_user.id)]
|
|
assert parsed_get_params['token'] == [
|
|
new_user.verification_key]
|
|
|
|
## Try verifying with bs verification key, shouldn't work
|
|
template.clear_test_template_context()
|
|
response = test_app.get(
|
|
"/auth/verify_email/?userid=%s&token=total_bs" % unicode(
|
|
new_user.id))
|
|
response.follow()
|
|
context = template.TEMPLATE_TEST_CONTEXT[
|
|
'mediagoblin/user_pages/user.html']
|
|
# assert context['verification_successful'] == True
|
|
# TODO: Would be good to test messages here when we can do so...
|
|
new_user = mg_globals.database.User.find_one(
|
|
{'username': u'happygirl'})
|
|
assert new_user
|
|
assert new_user.status == u'needs_email_verification'
|
|
assert new_user.email_verified == False
|
|
|
|
## Verify the email activation works
|
|
template.clear_test_template_context()
|
|
response = test_app.get("%s?%s" % (path, get_params))
|
|
response.follow()
|
|
context = template.TEMPLATE_TEST_CONTEXT[
|
|
'mediagoblin/user_pages/user.html']
|
|
# assert context['verification_successful'] == True
|
|
# TODO: Would be good to test messages here when we can do so...
|
|
new_user = mg_globals.database.User.find_one(
|
|
{'username': u'happygirl'})
|
|
assert new_user
|
|
assert new_user.status == u'active'
|
|
assert new_user.email_verified == True
|
|
|
|
# Uniqueness checks
|
|
# -----------------
|
|
## We shouldn't be able to register with that user twice
|
|
template.clear_test_template_context()
|
|
response = test_app.post(
|
|
'/auth/register/', {
|
|
'username': u'happygirl',
|
|
'password': 'iamsohappy2',
|
|
'email': 'happygrrl2@example.org'})
|
|
|
|
context = template.TEMPLATE_TEST_CONTEXT[
|
|
'mediagoblin/auth/register.html']
|
|
form = context['register_form']
|
|
assert form.username.errors == [
|
|
u'Sorry, a user with that name already exists.']
|
|
|
|
## TODO: Also check for double instances of an email address?
|
|
|
|
### Oops, forgot the password
|
|
# -------------------
|
|
template.clear_test_template_context()
|
|
response = test_app.post(
|
|
'/auth/forgot_password/',
|
|
{'username': u'happygirl'})
|
|
response.follow()
|
|
|
|
## Did we redirect to the proper page? Use the right template?
|
|
assert_equal(
|
|
urlparse.urlsplit(response.location)[2],
|
|
'/auth/login/')
|
|
assert template.TEMPLATE_TEST_CONTEXT.has_key(
|
|
'mediagoblin/auth/login.html')
|
|
|
|
## Make sure link to change password is sent by email
|
|
assert len(mail.EMAIL_TEST_INBOX) == 1
|
|
message = mail.EMAIL_TEST_INBOX.pop()
|
|
assert message['To'] == 'happygrrl@example.org'
|
|
email_context = template.TEMPLATE_TEST_CONTEXT[
|
|
'mediagoblin/auth/fp_verification_email.txt']
|
|
#TODO - change the name of verification_url to something forgot-password-ish
|
|
assert email_context['verification_url'] in message.get_payload(decode=True)
|
|
|
|
path = urlparse.urlsplit(email_context['verification_url'])[2]
|
|
get_params = urlparse.urlsplit(email_context['verification_url'])[3]
|
|
assert path == u'/auth/forgot_password/verify/'
|
|
parsed_get_params = urlparse.parse_qs(get_params)
|
|
|
|
# user should have matching parameters
|
|
new_user = mg_globals.database.User.find_one({'username': u'happygirl'})
|
|
assert parsed_get_params['userid'] == [unicode(new_user.id)]
|
|
assert parsed_get_params['token'] == [new_user.fp_verification_key]
|
|
|
|
### The forgotten password token should be set to expire in ~ 10 days
|
|
# A few ticks have expired so there are only 9 full days left...
|
|
assert (new_user.fp_token_expire - datetime.datetime.now()).days == 9
|
|
|
|
## Try using a bs password-changing verification key, shouldn't work
|
|
template.clear_test_template_context()
|
|
response = test_app.get(
|
|
"/auth/forgot_password/verify/?userid=%s&token=total_bs" % unicode(
|
|
new_user.id), status=404)
|
|
assert_equal(response.status.split()[0], u'404') # status="404 NOT FOUND"
|
|
|
|
## Try using an expired token to change password, shouldn't work
|
|
template.clear_test_template_context()
|
|
new_user = mg_globals.database.User.find_one({'username': u'happygirl'})
|
|
real_token_expiration = new_user.fp_token_expire
|
|
new_user.fp_token_expire = datetime.datetime.now()
|
|
new_user.save()
|
|
response = test_app.get("%s?%s" % (path, get_params), status=404)
|
|
assert_equal(response.status.split()[0], u'404') # status="404 NOT FOUND"
|
|
new_user.fp_token_expire = real_token_expiration
|
|
new_user.save()
|
|
|
|
## Verify step 1 of password-change works -- can see form to change password
|
|
template.clear_test_template_context()
|
|
response = test_app.get("%s?%s" % (path, get_params))
|
|
assert template.TEMPLATE_TEST_CONTEXT.has_key('mediagoblin/auth/change_fp.html')
|
|
|
|
## Verify step 2.1 of password-change works -- report success to user
|
|
template.clear_test_template_context()
|
|
response = test_app.post(
|
|
'/auth/forgot_password/verify/', {
|
|
'userid': parsed_get_params['userid'],
|
|
'password': 'iamveryveryhappy',
|
|
'token': parsed_get_params['token']})
|
|
response.follow()
|
|
assert template.TEMPLATE_TEST_CONTEXT.has_key(
|
|
'mediagoblin/auth/login.html')
|
|
|
|
## Verify step 2.2 of password-change works -- login w/ new password success
|
|
template.clear_test_template_context()
|
|
response = test_app.post(
|
|
'/auth/login/', {
|
|
'username': u'happygirl',
|
|
'password': 'iamveryveryhappy'})
|
|
|
|
# User should be redirected
|
|
response.follow()
|
|
assert_equal(
|
|
urlparse.urlsplit(response.location)[2],
|
|
'/')
|
|
assert template.TEMPLATE_TEST_CONTEXT.has_key(
|
|
'mediagoblin/root.html')
|
|
|
|
|
|
def test_authentication_views():
|
|
"""
|
|
Test logging in and logging out
|
|
"""
|
|
test_app = get_app(dump_old_app=False)
|
|
# Make a new user
|
|
test_user = fixture_add_user(active_user=False)
|
|
|
|
# Get login
|
|
# ---------
|
|
test_app.get('/auth/login/')
|
|
assert template.TEMPLATE_TEST_CONTEXT.has_key(
|
|
'mediagoblin/auth/login.html')
|
|
|
|
# Failed login - blank form
|
|
# -------------------------
|
|
template.clear_test_template_context()
|
|
response = test_app.post('/auth/login/')
|
|
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
|
|
form = context['login_form']
|
|
assert form.username.errors == [u'This field is required.']
|
|
assert form.password.errors == [u'This field is required.']
|
|
|
|
# Failed login - blank user
|
|
# -------------------------
|
|
template.clear_test_template_context()
|
|
response = test_app.post(
|
|
'/auth/login/', {
|
|
'password': u'toast'})
|
|
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
|
|
form = context['login_form']
|
|
assert form.username.errors == [u'This field is required.']
|
|
|
|
# Failed login - blank password
|
|
# -----------------------------
|
|
template.clear_test_template_context()
|
|
response = test_app.post(
|
|
'/auth/login/', {
|
|
'username': u'chris'})
|
|
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
|
|
form = context['login_form']
|
|
assert form.password.errors == [u'This field is required.']
|
|
|
|
# Failed login - bad user
|
|
# -----------------------
|
|
template.clear_test_template_context()
|
|
response = test_app.post(
|
|
'/auth/login/', {
|
|
'username': u'steve',
|
|
'password': 'toast'})
|
|
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
|
|
assert context['login_failed']
|
|
|
|
# Failed login - bad password
|
|
# ---------------------------
|
|
template.clear_test_template_context()
|
|
response = test_app.post(
|
|
'/auth/login/', {
|
|
'username': u'chris',
|
|
'password': 'jam_and_ham'})
|
|
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/auth/login.html']
|
|
assert context['login_failed']
|
|
|
|
# Successful login
|
|
# ----------------
|
|
template.clear_test_template_context()
|
|
response = test_app.post(
|
|
'/auth/login/', {
|
|
'username': u'chris',
|
|
'password': 'toast'})
|
|
|
|
# User should be redirected
|
|
response.follow()
|
|
assert_equal(
|
|
urlparse.urlsplit(response.location)[2],
|
|
'/')
|
|
assert template.TEMPLATE_TEST_CONTEXT.has_key(
|
|
'mediagoblin/root.html')
|
|
|
|
# Make sure user is in the session
|
|
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html']
|
|
session = context['request'].session
|
|
assert session['user_id'] == unicode(test_user.id)
|
|
|
|
# Successful logout
|
|
# -----------------
|
|
template.clear_test_template_context()
|
|
response = test_app.get('/auth/logout/')
|
|
|
|
# Should be redirected to index page
|
|
response.follow()
|
|
assert_equal(
|
|
urlparse.urlsplit(response.location)[2],
|
|
'/')
|
|
assert template.TEMPLATE_TEST_CONTEXT.has_key(
|
|
'mediagoblin/root.html')
|
|
|
|
# Make sure the user is not in the session
|
|
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/root.html']
|
|
session = context['request'].session
|
|
assert session.has_key('user_id') == False
|
|
|
|
# User is redirected to custom URL if POST['next'] is set
|
|
# -------------------------------------------------------
|
|
template.clear_test_template_context()
|
|
response = test_app.post(
|
|
'/auth/login/', {
|
|
'username': u'chris',
|
|
'password': 'toast',
|
|
'next' : '/u/chris/'})
|
|
assert_equal(
|
|
urlparse.urlsplit(response.location)[2],
|
|
'/u/chris/')
|
|
|