Django Secret Question suggestion
Saturday, 28th November, 2009 // 5:17 p.m.
PreviousNOTE: The data we set in a session is always in our session store not in user's cookie. I just missed just a simple point. Apologies. Thanks Kaihola for clarification.
Thank's bryan for proof reading.
It's been a while since I blogged and I wanted to get some feedback on the approach I took to solve a password reset problem I came across in a django project.
Situation:
So there is a model SecretQuestion which has a OnetoOneField relation to the User model.
Now when a user requests for password reset the application asks for the email of the user and checks if its associated with a registered user. If it is a valid associated email, it should redirect to a page which shows the user's secret question and prompts them to answer it. If answered correctly, we send an email to the user with a password reset link.
For this process I reused the Django's password reset view, but only after I handled the secret question. Here is the view which shows a email form, finds the associated user for that email, and present a secret question form
#!/usr/bin/python def forgot_password(request): if request.method == 'POST': password_form = ForgotPasswordEmailForm(request.POST) if password_form.is_valid(): user = User.objects.get(email__iexact=password_form.cleaned_data['email']) try: secret = user.secret_question secret_encoded = zlib.compress(pickle.dumps(str(secret.id), 0)).encode('base64').replace('\n', '') request.session['secret'] = secret_encoded return HttpResponseRedirect('/account/password/secret/') else: password_form = ForgotPasswordEmailForm() return render_to_response('account/forgot_password.html', {'password_form': password_form}, context_instance=RequestContext(request))
When I find the associated user for the email, I can get that user's secret question with "user.secretquestion" as SecretQuestion has OnetoOne relation with User. Now comes the important part where I encode the secret question id, put into the request.session object, and redirect to the url which serves the secret question view.
Since I am just using simple base64 encoding it is not very secure. Is there any other better reversible encoding algo which associates with a secret string or something?
Once we get done with the encoding, creating the secret question view is as simple as decoding the secret question id from the session object and presenting the question form. Here is the snippet of my secret question view:
#!/usr/bin/python def secret_answer(request): if not request.session.has_key('secret') and request.session['secret']: return HttpResponseRedirect('/login/') text = request.session['secret'] data = int(pickle.loads(zlib.decompress(text.decode('base64')))) secret_object = get_object_or_404(SecretQuestion, pk=data) user = secret_object.user question = secret_object.question message = '' if request.method == 'POST': answer_form = SecretAnswerForm(request.POST) if answer_form.is_valid(): answer = answer_form.cleaned_data['answer'] if secret_object.response == answer: from django.contrib.auth.tokens import default_token_generator as token_generator from django.utils.http import int_to_base36 notification.send([user], "change_password", {"user": user, 'uid': int_to_base36(user.id), 'token': token_generator.make_token(user), 'site': Site.objects.get_current().domain}) del request.session['secret'] return render_to_response('account/password_reset_successful.html', context_instance=RequestContext(request)) else: message = 'The Answer does not match with our records' else: answer_form = SecretAnswerForm() return render_to_response('account/secret_answer.html', {'question': question, 'answer_form': answer_form, 'message': message}, context_instance=RequestContext(request))
Yippy! it's great untill here, but what if the user tampers the data inside the cookie and change the secret question id? Your application is hacked. Django doesn't have signed cookies implemented yet. May be 1.2.
Update:
We can create a MD5 hash of with the secret question id and a new secret key. In this case, if the cookie's secret_question_is is tampered with, the MD5 hash won't match. Here is the modified forgot_password view:
#!/usr/bin/python ... from django.conf import settings request.session['secret'] = secret_encoded request.session['secret_hash'] = md5.new(str(secret.id)+settings.SECRET_KEY).hexdigest() ... #!/usr/bin/python ... data = int(pickle.loads(zlib.decompress(text.decode('base64')))) if not request.session['secret_hash'] == md5.new(request.session['secret']+settings.SECRET_KEY).hexdigest(): request.user.message_set.create(message="Do you think this app was written in PHP? No") redirect_to('/some/url/here') ...
When we want to have secure information passed between the server and a client without being exposed to the user, a cookie is the way to go. Just make sure it is securely signed. Also, check out Simon's proposal on adding signed cookies to Django.
I think there's a misunderstanding about Django's session handling here:
The values you store into request.session are not stored in the cookie. The cookie only contains a session key pointing to data in the database.
For this reason it doesn't make sense to pickle, base64encode and zlib.compress the question object. Instead, I'd just store the user ID in the session and retrieve the user object based on that in the question/answer view. The question object is then accessible through user.secret_question.
A visitor can't tamper with actual session data, only try to use a stolen session key.
Secret questions should probably be avoided altogether. See:
http://www.owasp.org/index.php/Guide_to_Authentication#Secret_Questions_And_Answers
If secret questions are a requirement, it's important to