From: Rodney Ewing Date: Fri, 17 May 2013 18:03:41 +0000 (-0700) Subject: moved fake_login_attempt to plugins X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=14efa7bdf19cf4042fe6455c0f9516f5b2d91a0c;p=mediagoblin.git moved fake_login_attempt to plugins --- diff --git a/mediagoblin/auth/__init__.py b/mediagoblin/auth/__init__.py index 60cbdb0e..ffa94212 100644 --- a/mediagoblin/auth/__init__.py +++ b/mediagoblin/auth/__init__.py @@ -49,3 +49,7 @@ def 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 fake_login_attempt(): + return hook_handle("auth_fake_login_attempt") diff --git a/mediagoblin/auth/lib.py b/mediagoblin/auth/lib.py index 45d0a63f..6f5340dd 100644 --- a/mediagoblin/auth/lib.py +++ b/mediagoblin/auth/lib.py @@ -13,35 +13,11 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - -import random - -import bcrypt - from mediagoblin.tools.mail import send_email from mediagoblin.tools.template import render_template from mediagoblin import mg_globals -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_VERIFICATION_TEMPLATE = ( u"http://{host}{uri}?" u"userid={userid}&token={verification_key}") diff --git a/mediagoblin/auth/views.py b/mediagoblin/auth/views.py index b13efebc..5be4b91b 100644 --- a/mediagoblin/auth/views.py +++ b/mediagoblin/auth/views.py @@ -118,7 +118,7 @@ def login(request): # Some failure during login occured if we are here! # Prevent detecting who's on this system by testing login # attempt timings - auth_lib.fake_login_attempt() + auth.fake_login_attempt() login_failed = True return render_to_response( @@ -209,3 +209,141 @@ def resend_activation(request): return redirect( request, 'mediagoblin.user_pages.user_home', user=request.user.username) + + +def forgot_password(request): + """ + Forgot password view + + Sends an email with an url to renew forgotten password. + Use GET querystring parameter 'username' to pre-populate the input field + """ + 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}) + + # 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 + # not reveal if the operation was successful then as we don't want to + # leak if an email address exists in the system. + found_by_email = '@' in fp_form.username.data + + if found_by_email: + user = User.query.filter_by( + email = fp_form.username.data).first() + # Don't reveal success in case the lookup happened by email address. + success_message=_("If that email address (case sensitive!) is " + "registered an email has been sent with instructions " + "on how to change your password.") + + else: # found by username + user = User.query.filter_by( + username = fp_form.username.data).first() + + if user is None: + messages.add_message(request, + messages.WARNING, + _("Couldn't find someone with that username.")) + return redirect(request, 'mediagoblin.auth.forgot_password') + + success_message=_("An email has been sent with instructions " + "on how to change your password.") + + if user and not(user.email_verified and user.status == 'active'): + # Don't send reminder because user is inactive or has no verified email + messages.add_message(request, + messages.WARNING, + _("Could not send password recovery email as your username is in" + "active or your account's email address has not been verified.")) + + return redirect(request, 'mediagoblin.user_pages.user_home', + user=user.username) + + # 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) + + messages.add_message(request, messages.INFO, success_message) + return redirect(request, 'mediagoblin.auth.login') + + +def verify_forgot_password(request): + """ + Check the forgot-password verification and possibly let the user + change their password because of it. + """ + # get form data variables, and specifically check for presence of token + formdata = _process_for_token(request) + if not formdata['has_userid_and_token']: + return render_404(request) + + formdata_token = formdata['vars']['token'] + formdata_userid = formdata['vars']['userid'] + formdata_vars = formdata['vars'] + + # check if it's a valid user id + user = User.query.filter_by(id=formdata_userid).first() + if not user: + return render_404(request) + + # 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')): + + cp_form = auth_forms.ChangePassForm(formdata_vars) + + if request.method == 'POST' and cp_form.validate(): + user.pw_hash = auth_lib.bcrypt_gen_password_hash( + cp_form.password.data) + user.fp_verification_key = None + user.fp_token_expire = None + user.save() + + messages.add_message( + request, + messages.INFO, + _("You can now log in using your new password.")) + return redirect(request, 'mediagoblin.auth.login') + else: + return render_to_response( + request, + 'mediagoblin/auth/change_fp.html', + {'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) + + +def _process_for_token(request): + """ + Checks for tokens in formdata without prior knowledge of request method + + For now, returns whether the userid and token formdata variables exist, and + the formdata variables in a hash. Perhaps an object is warranted? + """ + # retrieve the formdata variables + if request.method == 'GET': + formdata_vars = request.GET + else: + formdata_vars = request.form + + formdata = { + 'vars': formdata_vars, + 'has_userid_and_token': + 'userid' in formdata_vars and 'token' in formdata_vars} + + return formdata diff --git a/mediagoblin/plugins/basic_auth/__init__.py b/mediagoblin/plugins/basic_auth/__init__.py index db5229a8..c8c3c391 100644 --- a/mediagoblin/plugins/basic_auth/__init__.py +++ b/mediagoblin/plugins/basic_auth/__init__.py @@ -102,4 +102,5 @@ hooks = { 'auth_get_login_form': get_login_form, 'auth_get_registration_form': get_registration_form, 'auth_gen_password_hash': gen_password_hash, + 'auth_fake_login_attempt': auth_lib.fake_login_attempt, } diff --git a/mediagoblin/plugins/basic_auth/lib.py b/mediagoblin/plugins/basic_auth/lib.py index 57820003..999abfd9 100644 --- a/mediagoblin/plugins/basic_auth/lib.py +++ b/mediagoblin/plugins/basic_auth/lib.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import bcrypt +import random from mediagoblin.tools.template import render_template from mediagoblin.tools.mail import send_email @@ -97,3 +98,22 @@ def send_fp_verification_email(user, request): [user.email], 'GNU MediaGoblin - Change forgotten password!', rendered_email) + + +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 diff --git a/mediagoblin/plugins/piwigo/views.py b/mediagoblin/plugins/piwigo/views.py index 78668ed4..7e3f1076 100644 --- a/mediagoblin/plugins/piwigo/views.py +++ b/mediagoblin/plugins/piwigo/views.py @@ -23,7 +23,6 @@ from werkzeug.exceptions import MethodNotAllowed, BadRequest, NotImplemented from werkzeug.wrappers import BaseResponse from mediagoblin.meddleware.csrf import csrf_exempt -from mediagoblin.auth.lib import fake_login_attempt from mediagoblin.media_types import sniff_media from mediagoblin.submit.lib import check_file_field, prepare_queue_task, \ run_process_media, new_upload_entry @@ -33,6 +32,7 @@ from mediagoblin.db.models import Collection from .tools import CmdTable, response_xml, check_form, \ PWGSession, PwgNamedArray, PwgError +from mediagoblin.plugins.basic_auth.lib import fake_login_attempt from .forms import AddSimpleForm, AddForm @@ -126,7 +126,7 @@ def pwg_images_addSimple(request): dump = [] for f in form: dump.append("%s=%r" % (f.name, f.data)) - _log.info("addSimple: %r %s %r", request.form, " ".join(dump), + _log.info("addSimple: %r %s %r", request.form, " ".join(dump), request.files) if not check_file_field(request, 'image'):