aboutsummaryrefslogtreecommitdiffstats
path: root/mediagoblin/auth
diff options
context:
space:
mode:
authortilly-Q <nattilypigeonfowl@gmail.com>2013-07-29 18:40:19 -0400
committertilly-Q <nattilypigeonfowl@gmail.com>2013-07-29 18:40:19 -0400
commit52a355b27541597fc155dab5e4885207b12a0a7b (patch)
tree7d00edf4d9e39a6b7e4c838ca6f7cb07a4d9a51c /mediagoblin/auth
parentf26c21cd5b7998c903fa67aaf164c07743fee651 (diff)
parent08cd10d84fd89df3f0cad3835ba8ab8b8000d4b2 (diff)
downloadmediagoblin-52a355b27541597fc155dab5e4885207b12a0a7b.tar.lz
mediagoblin-52a355b27541597fc155dab5e4885207b12a0a7b.tar.xz
mediagoblin-52a355b27541597fc155dab5e4885207b12a0a7b.zip
Merge branch 'ticket-679' into OPW-Moderation-Update
Conflicts: mediagoblin/auth/tools.py mediagoblin/auth/views.py mediagoblin/db/migration_tools.py mediagoblin/db/migrations.py mediagoblin/db/models.py mediagoblin/decorators.py mediagoblin/user_pages/views.py
Diffstat (limited to 'mediagoblin/auth')
-rw-r--r--mediagoblin/auth/__init__.py29
-rw-r--r--mediagoblin/auth/forms.py29
-rw-r--r--mediagoblin/auth/lib.py120
-rw-r--r--mediagoblin/auth/tools.py119
-rw-r--r--mediagoblin/auth/views.py139
5 files changed, 205 insertions, 231 deletions
diff --git a/mediagoblin/auth/__init__.py b/mediagoblin/auth/__init__.py
index 621845ba..be5d0eed 100644
--- a/mediagoblin/auth/__init__.py
+++ b/mediagoblin/auth/__init__.py
@@ -13,3 +13,32 @@
#
# 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/>.
+from mediagoblin.tools.pluginapi import hook_handle, hook_runall
+
+
+def get_user(**kwargs):
+ """ Takes a kwarg such as username and returns a user object """
+ return hook_handle("auth_get_user", **kwargs)
+
+
+def create_user(register_form):
+ results = hook_runall("auth_create_user", register_form)
+ return results[0]
+
+
+def extra_validation(register_form):
+ from mediagoblin.auth.tools import basic_extra_validation
+
+ extra_validation_passes = basic_extra_validation(register_form)
+ if False in hook_runall("auth_extra_validation", register_form):
+ extra_validation_passes = False
+ return extra_validation_passes
+
+
+def gen_password_hash(raw_pass, extra_salt=None):
+ return hook_handle("auth_gen_password_hash", raw_pass, extra_salt)
+
+
+def check_password(raw_pass, stored_hash, extra_salt=None):
+ return hook_handle("auth_check_password",
+ raw_pass, stored_hash, extra_salt)
diff --git a/mediagoblin/auth/forms.py b/mediagoblin/auth/forms.py
index 0a391d67..865502e9 100644
--- a/mediagoblin/auth/forms.py
+++ b/mediagoblin/auth/forms.py
@@ -20,32 +20,6 @@ from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
from mediagoblin.auth.tools import normalize_user_or_email_field
-class RegistrationForm(wtforms.Form):
- username = wtforms.TextField(
- _('Username'),
- [wtforms.validators.Required(),
- normalize_user_or_email_field(allow_email=False)])
- password = wtforms.PasswordField(
- _('Password'),
- [wtforms.validators.Required(),
- wtforms.validators.Length(min=5, max=1024)])
- email = wtforms.TextField(
- _('Email address'),
- [wtforms.validators.Required(),
- normalize_user_or_email_field(allow_user=False)])
-
-
-class LoginForm(wtforms.Form):
- username = wtforms.TextField(
- _('Username or Email'),
- [wtforms.validators.Required(),
- normalize_user_or_email_field()])
- password = wtforms.PasswordField(
- _('Password'),
- [wtforms.validators.Required(),
- wtforms.validators.Length(min=5, max=1024)])
-
-
class ForgotPassForm(wtforms.Form):
username = wtforms.TextField(
_('Username or email'),
@@ -58,9 +32,6 @@ class ChangePassForm(wtforms.Form):
'Password',
[wtforms.validators.Required(),
wtforms.validators.Length(min=5, max=1024)])
- userid = wtforms.HiddenField(
- '',
- [wtforms.validators.Required()])
token = wtforms.HiddenField(
'',
[wtforms.validators.Required()])
diff --git a/mediagoblin/auth/lib.py b/mediagoblin/auth/lib.py
deleted file mode 100644
index bfc36b28..00000000
--- a/mediagoblin/auth/lib.py
+++ /dev/null
@@ -1,120 +0,0 @@
-# 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 random
-
-import bcrypt
-
-from mediagoblin.tools.mail import send_email
-from mediagoblin.tools.template import render_template
-from mediagoblin import mg_globals
-
-
-def bcrypt_check_password(raw_pass, stored_hash, extra_salt=None):
- """
- Check to see if this password matches.
-
- Args:
- - raw_pass: user submitted password to check for authenticity.
- - stored_hash: The hash of the raw password (and possibly extra
- salt) to check against
- - extra_salt: (optional) If this password is with stored with a
- non-database extra salt (probably in the config file) for extra
- security, factor this into the check.
-
- Returns:
- True or False depending on success.
- """
- if extra_salt:
- raw_pass = u"%s:%s" % (extra_salt, raw_pass)
-
- hashed_pass = bcrypt.hashpw(raw_pass.encode('utf-8'), stored_hash)
-
- # Reduce risk of timing attacks by hashing again with a random
- # number (thx to zooko on this advice, which I hopefully
- # incorporated right.)
- #
- # See also:
- rand_salt = bcrypt.gensalt(5)
- randplus_stored_hash = bcrypt.hashpw(stored_hash, rand_salt)
- randplus_hashed_pass = bcrypt.hashpw(hashed_pass, rand_salt)
-
- return randplus_stored_hash == randplus_hashed_pass
-
-
-def bcrypt_gen_password_hash(raw_pass, extra_salt=None):
- """
- Generate a salt for this new password.
-
- Args:
- - raw_pass: user submitted password
- - extra_salt: (optional) If this password is with stored with a
- non-database extra salt
- """
- if extra_salt:
- raw_pass = u"%s:%s" % (extra_salt, raw_pass)
-
- return unicode(
- bcrypt.hashpw(raw_pass.encode('utf-8'), bcrypt.gensalt()))
-
-
-def fake_login_attempt():
- """
- Pretend we're trying to login.
-
- Nothing actually happens here, we're just trying to take up some
- time, approximately the same amount of time as
- bcrypt_check_password, so as to avoid figuring out what users are
- on the system by intentionally faking logins a bunch of times.
- """
- rand_salt = bcrypt.gensalt(5)
-
- hashed_pass = bcrypt.hashpw(str(random.random()), rand_salt)
-
- randplus_stored_hash = bcrypt.hashpw(str(random.random()), rand_salt)
- randplus_hashed_pass = bcrypt.hashpw(hashed_pass, rand_salt)
-
- randplus_stored_hash == randplus_hashed_pass
-
-
-EMAIL_FP_VERIFICATION_TEMPLATE = (
- u"http://{host}{uri}?"
- u"userid={userid}&token={fp_verification_key}")
-
-
-def send_fp_verification_email(user, request):
- """
- Send the verification email to users to change their password.
-
- Args:
- - user: a user object
- - request: the request
- """
- rendered_email = render_template(
- request, 'mediagoblin/auth/fp_verification_email.txt',
- {'username': user.username,
- 'verification_url': EMAIL_FP_VERIFICATION_TEMPLATE.format(
- host=request.host,
- uri=request.urlgen('mediagoblin.auth.verify_forgot_password'),
- userid=unicode(user.id),
- fp_verification_key=user.fp_verification_key)})
-
- # TODO: There is no error handling in place
- send_email(
- mg_globals.app_config['email_sender_address'],
- [user.email],
- 'GNU MediaGoblin - Change forgotten password!',
- rendered_email)
diff --git a/mediagoblin/auth/tools.py b/mediagoblin/auth/tools.py
index 39b349de..596a4447 100644
--- a/mediagoblin/auth/tools.py
+++ b/mediagoblin/auth/tools.py
@@ -14,19 +14,20 @@
# 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 uuid
-import logging
+import logging
import wtforms
from sqlalchemy import or_
from mediagoblin import mg_globals
-from mediagoblin.auth import lib as auth_lib
-from mediagoblin.db.models import User, Privilege
+from mediagoblin.tools.crypto import get_timed_signer_url
+from mediagoblin.db.models import User
from mediagoblin.tools.mail import (normalize_email, send_email,
email_debug_message)
from mediagoblin.tools.template import render_template
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
+from mediagoblin.tools.pluginapi import hook_handle
+from mediagoblin import auth
_log = logging.getLogger(__name__)
@@ -62,11 +63,12 @@ def normalize_user_or_email_field(allow_email=True, allow_user=True):
EMAIL_VERIFICATION_TEMPLATE = (
- u"http://{host}{uri}?"
- u"userid={userid}&token={verification_key}")
+ u"{uri}?"
+ u"token={verification_key}")
-def send_verification_email(user, request):
+def send_verification_email(user, request, email=None,
+ rendered_email=None):
"""
Send the verification email to users to activate their accounts.
@@ -74,19 +76,24 @@ def send_verification_email(user, request):
- user: a user object
- request: the request
"""
- rendered_email = render_template(
- request, 'mediagoblin/auth/verification_email.txt',
- {'username': user.username,
- 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
- host=request.host,
- uri=request.urlgen('mediagoblin.auth.verify_email'),
- userid=unicode(user.id),
- verification_key=user.verification_key)})
+ if not email:
+ email = user.email
+
+ if not rendered_email:
+ verification_key = get_timed_signer_url('mail_verification_token') \
+ .dumps(user.id)
+ rendered_email = render_template(
+ request, 'mediagoblin/auth/verification_email.txt',
+ {'username': user.username,
+ 'verification_url': EMAIL_VERIFICATION_TEMPLATE.format(
+ uri=request.urlgen('mediagoblin.auth.verify_email',
+ qualified=True),
+ verification_key=verification_key)})
# TODO: There is no error handling in place
send_email(
mg_globals.app_config['email_sender_address'],
- [user.email],
+ [email],
# TODO
# Due to the distributed nature of GNU MediaGoblin, we should
# find a way to send some additional information about the
@@ -96,11 +103,43 @@ def send_verification_email(user, request):
rendered_email)
+EMAIL_FP_VERIFICATION_TEMPLATE = (
+ u"{uri}?"
+ u"token={fp_verification_key}")
+
+
+def send_fp_verification_email(user, request):
+ """
+ Send the verification email to users to change their password.
+
+ Args:
+ - user: a user object
+ - request: the request
+ """
+ fp_verification_key = get_timed_signer_url('mail_verification_token') \
+ .dumps(user.id)
+
+ rendered_email = render_template(
+ request, 'mediagoblin/auth/fp_verification_email.txt',
+ {'username': user.username,
+ 'verification_url': EMAIL_FP_VERIFICATION_TEMPLATE.format(
+ uri=request.urlgen('mediagoblin.auth.verify_forgot_password',
+ qualified=True),
+ fp_verification_key=fp_verification_key)})
+
+ # TODO: There is no error handling in place
+ send_email(
+ mg_globals.app_config['email_sender_address'],
+ [user.email],
+ 'GNU MediaGoblin - Change forgotten password!',
+ rendered_email)
+
+
def basic_extra_validation(register_form, *args):
users_with_username = User.query.filter_by(
- username=register_form.data['username']).count()
+ username=register_form.username.data).count()
users_with_email = User.query.filter_by(
- email=register_form.data['email']).count()
+ email=register_form.email.data).count()
extra_validation_passes = True
@@ -118,17 +157,11 @@ def basic_extra_validation(register_form, *args):
def register_user(request, register_form):
""" Handle user registration """
- extra_validation_passes = basic_extra_validation(register_form)
+ extra_validation_passes = auth.extra_validation(register_form)
if extra_validation_passes:
# Create the user
- user = User()
- user.username = register_form.data['username']
- user.email = register_form.data['email']
- user.pw_hash = auth_lib.bcrypt_gen_password_hash(
- register_form.password.data)
- user.verification_key = unicode(uuid.uuid4())
- user.save()
+ user = auth.create_user(register_form)
# give the user the default privileges
default_privileges = [
@@ -151,17 +184,37 @@ def register_user(request, register_form):
return None
-def check_login_simple(username, password, username_might_be_email=False):
- search = (User.username == username)
- if username_might_be_email and ('@' in username):
- search = or_(search, User.email == username)
- user = User.query.filter(search).first()
+def check_login_simple(username, password):
+ user = auth.get_user(username=username)
if not user:
_log.info("User %r not found", username)
- auth_lib.fake_login_attempt()
+ hook_handle("auth_fake_login_attempt")
return None
- if not auth_lib.bcrypt_check_password(password, user.pw_hash):
+ if not auth.check_password(password, user.pw_hash):
_log.warn("Wrong password for %r", username)
return None
_log.info("Logging %r in", username)
return user
+
+
+def check_auth_enabled():
+ if not hook_handle('authentication'):
+ _log.warning('No authentication is enabled')
+ return False
+ else:
+ return True
+
+
+def no_auth_logout(request):
+ """Log out the user if authentication_disabled, but don't delete the messages"""
+ if not mg_globals.app.auth and 'user_id' in request.session:
+ del request.session['user_id']
+ request.session.save()
+
+
+def create_basic_user(form):
+ user = User()
+ user.username = form.username.data
+ user.email = form.email.data
+ user.save()
+ return user
diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py
index 1c346556..90d6f5cc 100644
--- a/mediagoblin/auth/views.py
+++ b/mediagoblin/auth/views.py
@@ -14,36 +14,37 @@
# 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 uuid
-import datetime
+from itsdangerous import BadSignature
from mediagoblin import messages, mg_globals
from mediagoblin.db.models import User, Privilege
+from mediagoblin.tools.crypto import get_timed_signer_url
+from mediagoblin.decorators import auth_enabled, allow_registration
from mediagoblin.tools.response import render_to_response, redirect, render_404
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.mail import email_debug_message
-from mediagoblin.auth import lib as auth_lib
+from mediagoblin.tools.pluginapi import hook_handle
from mediagoblin.auth import forms as auth_forms
-from mediagoblin.auth.lib import send_fp_verification_email
from mediagoblin.auth.tools import (send_verification_email, register_user,
+ send_fp_verification_email,
check_login_simple)
+from mediagoblin import auth
+@allow_registration
+@auth_enabled
def register(request):
"""The registration view.
Note that usernames will always be lowercased. Email domains are lowercased while
the first part remains case-sensitive.
"""
- # Redirects to indexpage if registrations are disabled
- if not mg_globals.app_config["allow_registration"]:
- messages.add_message(
- request,
- messages.WARNING,
- _('Sorry, registration is disabled on this instance.'))
- return redirect(request, "index")
+ if 'pass_auth' not in request.template_env.globals:
+ redirect_name = hook_handle('auth_no_pass_redirect')
+ return redirect(request, 'mediagoblin.plugins.{0}.register'.format(
+ redirect_name))
- register_form = auth_forms.RegistrationForm(request.form)
+ register_form = hook_handle("auth_get_registration_form", request)
if request.method == 'POST' and register_form.validate():
# TODO: Make sure the user doesn't exist already
@@ -59,28 +60,36 @@ def register(request):
return render_to_response(
request,
'mediagoblin/auth/register.html',
- {'register_form': register_form})
+ {'register_form': register_form,
+ 'post_url': request.urlgen('mediagoblin.auth.register')})
+@auth_enabled
def login(request):
"""
MediaGoblin login view.
If you provide the POST with 'next', it'll redirect to that view.
"""
- login_form = auth_forms.LoginForm(request.form)
+ if 'pass_auth' not in request.template_env.globals:
+ redirect_name = hook_handle('auth_no_pass_redirect')
+ return redirect(request, 'mediagoblin.plugins.{0}.login'.format(
+ redirect_name))
+
+ login_form = hook_handle("auth_get_login_form", request)
login_failed = False
if request.method == 'POST':
-
- username = login_form.data['username']
+ username = login_form.username.data
if login_form.validate():
- user = check_login_simple(username, login_form.password.data, True)
+ user = check_login_simple(username, login_form.password.data)
if user:
# set up login in session
+ if login_form.stay_logged_in.data:
+ request.session['stay_logged_in'] = True
request.session['user_id'] = unicode(user.id)
request.session.save()
@@ -97,6 +106,7 @@ def login(request):
{'login_form': login_form,
'next': request.GET.get('next') or request.form.get('next'),
'login_failed': login_failed,
+ 'post_url': request.urlgen('mediagoblin.auth.login'),
'allow_registration': mg_globals.app_config["allow_registration"]})
@@ -115,12 +125,26 @@ def verify_email(request):
you are lucky :)
"""
# If we don't have userid and token parameters, we can't do anything; 404
- if not 'userid' in request.GET or not 'token' in request.GET:
+ if not 'token' in request.GET:
return render_404(request)
- user = User.query.filter_by(id=request.args['userid']).first()
+ # Catch error if token is faked or expired
+ try:
+ token = get_timed_signer_url("mail_verification_token") \
+ .loads(request.GET['token'], max_age=10*24*3600)
+ except BadSignature:
+ messages.add_message(
+ request,
+ messages.ERROR,
+ _('The verification key or user id is incorrect.'))
+
+ return redirect(
+ request,
+ 'index')
+
+ user = User.query.filter_by(id=int(token)).first()
- if user and user.verification_key == unicode(request.GET['token']):
+ if user and user.email_verified is False:
user.status = u'active'
user.email_verified = True
user.verification_key = None
@@ -169,9 +193,6 @@ def resend_activation(request):
return redirect(request, "mediagoblin.user_pages.user_home", user=request.user['username'])
- request.user.verification_key = unicode(uuid.uuid4())
- request.user.save()
-
email_debug_message(request)
send_verification_email(request.user, request)
@@ -191,13 +212,16 @@ def forgot_password(request):
Sends an email with an url to renew forgotten password.
Use GET querystring parameter 'username' to pre-populate the input field
"""
+ if not 'pass_auth' in request.template_env.globals:
+ return redirect(request, 'index')
+
fp_form = auth_forms.ForgotPassForm(request.form,
username=request.args.get('username'))
if not (request.method == 'POST' and fp_form.validate()):
# Either GET request, or invalid form submitted. Display the template
return render_to_response(request,
- 'mediagoblin/auth/forgot_password.html', {'fp_form': fp_form})
+ 'mediagoblin/auth/forgot_password.html', {'fp_form': fp_form,})
# If we are here: method == POST and form is valid. username casing
# has been sanitized. Store if a user was found by email. We should
@@ -238,11 +262,6 @@ def forgot_password(request):
# SUCCESS. Send reminder and return to login page
if user:
- user.fp_verification_key = unicode(uuid.uuid4())
- user.fp_token_expire = datetime.datetime.now() + \
- datetime.timedelta(days=10)
- user.save()
-
email_debug_message(request)
send_fp_verification_email(user, request)
@@ -257,31 +276,44 @@ def verify_forgot_password(request):
"""
# get form data variables, and specifically check for presence of token
formdata = _process_for_token(request)
- if not formdata['has_userid_and_token']:
+ if not formdata['has_token']:
return render_404(request)
- formdata_token = formdata['vars']['token']
- formdata_userid = formdata['vars']['userid']
formdata_vars = formdata['vars']
+ # Catch error if token is faked or expired
+ try:
+ token = get_timed_signer_url("mail_verification_token") \
+ .loads(formdata_vars['token'], max_age=10*24*3600)
+ except BadSignature:
+ messages.add_message(
+ request,
+ messages.ERROR,
+ _('The verification key or user id is incorrect.'))
+
+ return redirect(
+ request,
+ 'index')
+
# check if it's a valid user id
- user = User.query.filter_by(id=formdata_userid).first()
+ user = User.query.filter_by(id=int(token)).first()
+
+ # no user in db
if not user:
- return render_404(request)
+ messages.add_message(
+ request, messages.ERROR,
+ _('The user id is incorrect.'))
+ return redirect(
+ request, 'index')
- # check if we have a real user and correct token
- if ((user and user.fp_verification_key and
- user.fp_verification_key == unicode(formdata_token) and
- datetime.datetime.now() < user.fp_token_expire
- and user.email_verified and user.status == 'active')):
+ # check if user active and has email verified
+ if user.email_verified and user.status == 'active':
cp_form = auth_forms.ChangePassForm(formdata_vars)
if request.method == 'POST' and cp_form.validate():
- user.pw_hash = auth_lib.bcrypt_gen_password_hash(
+ user.pw_hash = auth.gen_password_hash(
cp_form.password.data)
- user.fp_verification_key = None
- user.fp_token_expire = None
user.save()
messages.add_message(
@@ -293,12 +325,22 @@ def verify_forgot_password(request):
return render_to_response(
request,
'mediagoblin/auth/change_fp.html',
- {'cp_form': cp_form})
+ {'cp_form': cp_form,})
- # in case there is a valid id but no user with that id in the db
- # or the token expired
- else:
- return render_404(request)
+ if not user.email_verified:
+ messages.add_message(
+ request, messages.ERROR,
+ _('You need to verify your email before you can reset your'
+ ' password.'))
+
+ if not user.status == 'active':
+ messages.add_message(
+ request, messages.ERROR,
+ _('You are no longer an active user. Please contact the system'
+ ' admin to reactivate your accoutn.'))
+
+ return redirect(
+ request, 'index')
def _process_for_token(request):
@@ -316,7 +358,6 @@ def _process_for_token(request):
formdata = {
'vars': formdata_vars,
- 'has_userid_and_token':
- 'userid' in formdata_vars and 'token' in formdata_vars}
+ 'has_token': 'token' in formdata_vars}
return formdata