Yashh

Django Secret Question suggestion

Saturday, 28th November, 2009 // 5:17 p.m.

NOTE: 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.

SecretQuestion Model Relation

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.

Tagged: django , password , question , reset , secret and session

1 comment:
  1. 001// Antti Kaihola// Monday, 28th December, 2009, 6:01 a.m.

    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

    • let the user pick the question
    • store a hash of the answer, not the answer itself
Comment Form

MarkDown syntax enabled