diff options
Diffstat (limited to 'mediagoblin/auth')
-rw-r--r-- | mediagoblin/auth/__init__.py | 47 | ||||
-rw-r--r-- | mediagoblin/auth/forms.py | 26 | ||||
-rw-r--r-- | mediagoblin/auth/lib.py | 120 | ||||
-rw-r--r-- | mediagoblin/auth/tools.py | 81 | ||||
-rw-r--r-- | mediagoblin/auth/views.py | 49 |
5 files changed, 153 insertions, 170 deletions
diff --git a/mediagoblin/auth/__init__.py b/mediagoblin/auth/__init__.py index 621845ba..53182eaa 100644 --- a/mediagoblin/auth/__init__.py +++ b/mediagoblin/auth/__init__.py @@ -13,3 +13,50 @@ # # 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 check_login(user, password): + result = hook_handle("auth_check_login", user, password) + if result: + return result + return False + + +def get_user(form): + return hook_handle("auth_get_user", form) + + +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 get_login_form(request): + return hook_handle("auth_get_login_form", request) + + +def get_registration_form(request): + return hook_handle("auth_get_registration_form", request) + + +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) + + +def fake_login_attempt(): + return hook_handle("auth_fake_login_attempt") diff --git a/mediagoblin/auth/forms.py b/mediagoblin/auth/forms.py index 0a391d67..7a67285b 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'), 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 db6b6e37..f38a292a 100644 --- a/mediagoblin/auth/tools.py +++ b/mediagoblin/auth/tools.py @@ -14,7 +14,6 @@ # 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 wtforms @@ -25,8 +24,12 @@ from mediagoblin.auth import lib as auth_lib 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.template import render_template +from mediagoblin.tools.pluginapi import hook_handle +from mediagoblin import auth + +_log = logging.getLogger(__name__) _log = logging.getLogger(__name__) @@ -61,6 +64,35 @@ def normalize_user_or_email_field(allow_email=True, allow_user=True): return _normalize_field +class AuthError(Exception): + def __init__(self): + self.value = 'No Authentication Plugin is enabled and no_auth = false'\ + ' in config!' + + def __str__(self): + return repr(self.value) + + +def check_auth_enabled(): + no_auth = mg_globals.app_config['no_auth'] + auth_plugin = hook_handle('authentication') + + if no_auth == 'false' and not auth_plugin: + raise AuthError + + if no_auth == 'true' and not auth_plugin: + _log.warning('No authentication is enabled') + return False + else: + return True + + +def no_auth_logout(request): + """Log out the user if in no_auth mode""" + if not mg_globals.app.auth: + request.session.delete() + + EMAIL_VERIFICATION_TEMPLATE = ( u"http://{host}{uri}?" u"userid={userid}&token={verification_key}") @@ -98,9 +130,9 @@ def send_verification_email(user, request): 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 +150,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) # log the user in request.session['user_id'] = unicode(user.id) @@ -137,12 +163,41 @@ def register_user(request, register_form): # send verification email email_debug_message(request) send_verification_email(user, request) - return user return None +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) + + def check_login_simple(username, password, username_might_be_email=False): search = (User.username == username) if username_might_be_email and ('@' in username): diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index bb7bda77..109763ce 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -24,9 +24,11 @@ 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.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, +from mediagoblin.auth.tools import (send_verification_email, + register_user, + send_fp_verification_email, check_login_simple) +from mediagoblin import auth def register(request): @@ -35,15 +37,20 @@ def register(request): 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"]: + # Redirects to indexpage if registrations are disabled or no authentication + # is enabled + if not mg_globals.app_config["allow_registration"] or not mg_globals.app.auth: messages.add_message( request, messages.WARNING, _('Sorry, registration is disabled on this instance.')) return redirect(request, "index") - register_form = auth_forms.RegistrationForm(request.form) + if 'pass_auth' not in request.template_env.globals: + if 'openid' in request.template_env.globals: + return redirect(request, 'mediagoblin.plugins.openid.register') + + register_form = auth.get_registration_form(request) if request.method == 'POST' and register_form.validate(): # TODO: Make sure the user doesn't exist already @@ -59,7 +66,9 @@ def register(request): return render_to_response( request, 'mediagoblin/auth/register.html', - {'register_form': register_form}) + {'register_form': register_form, + 'focus': 'username', + 'post_url': request.urlgen('mediagoblin.auth.register')}) def login(request): @@ -68,13 +77,24 @@ def login(request): If you provide the POST with 'next', it'll redirect to that view. """ - login_form = auth_forms.LoginForm(request.form) + # Redirects to index page if no authentication is enabled + if not mg_globals.app.auth: + messages.add_message( + request, + messages.WARNING, + _('Sorry, authentication is disabled on this instance.')) + return redirect(request, 'index') + + if 'pass_auth' not in request.template_env.globals: + if 'openid' in request.template_env.globals: + return redirect(request, 'mediagoblin.plugins.openid.login') + + login_form = 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) @@ -97,6 +117,8 @@ def login(request): {'login_form': login_form, 'next': request.GET.get('next') or request.form.get('next'), 'login_failed': login_failed, + 'focus': 'username', + 'post_url': request.urlgen('mediagoblin.auth.login'), 'allow_registration': mg_globals.app_config["allow_registration"]}) @@ -188,13 +210,17 @@ 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, + 'focus': 'username'}) # 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 @@ -290,7 +316,8 @@ def verify_forgot_password(request): return render_to_response( request, 'mediagoblin/auth/change_fp.html', - {'cp_form': cp_form}) + {'cp_form': cp_form, + 'focus': 'password'}) # in case there is a valid id but no user with that id in the db # or the token expired |