Sumit
Sumit

Reputation: 328

Validating captcha in Flask

I am creating a captcha just for an exercise. The creation of the captcha images seem fine. But every time I try validating the captcha challenge entered by the user, the validation is done against the next captcha. I am stuck at how to go with this. Function for creating captcha images- captcha.py

import random
import Image
import ImageFont
import ImageDraw
import ImageFilter
import JpegImagePlugin
import PngImagePlugin


def gen_captcha(text, fnt, fnt_sz, file_name, fmt='JPEG'):
fgcolor = random.randint(0,0xff0000)
bgcolor = fgcolor ^ 0xffffff
font = ImageFont.truetype(fnt,fnt_sz)
dim = font.getsize(text)
im = Image.new('RGB', (dim[0]+5,dim[1]+5), bgcolor)
d = ImageDraw.Draw(im)
x, y = im.size
r = random.randint
for num in range(100):
    d.rectangle((r(0,x),r(0,y),r(0,x),r(0,y)),fill=r(0,0xffff00))
d.text((3,3), text, font=font, fill=fgcolor)
im = im.filter(ImageFilter.EDGE_ENHANCE_MORE)
im.save(file_name)

signup function from views.py

@app.route('/signup', methods = ['GET', 'POST'])
def signup():
if g.user is not None and g.user.is_authenticated():
    return redirect(url_for('index'))

words = open('app/corncob_caps.txt').readlines()
captcha_word = words[random.randint(1,len(words))]
captcha_filename = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(10)) + '.jpg'
captcha.gen_captcha(captcha_word.strip(), 'app/os.ttf', 25, 'app/static/' + captcha_filename + '')


form = SignUpForm(captcha_word)

if form.validate_on_submit() == False:
    return render_template('signup.html', form = form, filename = captcha_filename)
else:
    user = User(form.email.data, form.password.data)
    db.session.add(user)
    db.session.commit()
    flash('You have successfully signed up.')
    flash('You may login now.')
    return redirect(url_for('login'))

return render_template('signup.html', form = form, filename = captcha_filename)

I am passing the captcha_word to my form class. The form class is:

class SignUpForm(Form):
email = EmailField('Email Address', validators = [email()])
password = PasswordField('Password', validators = [Required('Please enter a valid password between 8 and 30 characters.'), Length(min = 8, max = 30)])
captcha = TextField('Captcha', validators = [Required('You must enter the challenge captcha.')])
submit = SubmitField('Create Account')
captcha_word = ''

def __init__(self, word, *args, **kwargs):
    Form.__init__(self, *args, **kwargs)
    self.get_word(word)

def get_word(self, word):
    self.captcha_word = word

def validate(self):
    if not Form.validate(self):
        return False
    elif self.captcha_word != self.captcha.data.upper():
        print self.captcha_word
        print self.captcha.data.upper()
        self.captcha.errors.append("Wrong captcha!")
        return False

    user = self.get_user()
    if user:
        self.email.errors.append("That email is already taken.")
        return False
    else:
        return True

def get_user(self):
    return User.query.filter_by(email = self.email.data.lower()).first()

I inserted the two print statements inside to see why the comparison was coming wrong. The first print showed the next captcha whereas the print self.captcha.data.upper() displayed the user entered data.

I am not sure, but it seems the signup route is being called twice. But I don't know how to fix this. Any ideas?

Upvotes: 2

Views: 10748

Answers (1)

Doobeh
Doobeh

Reputation: 9440

If you need to use a captcha, you can use the feature that's already built into Flask-WTF and save yourself reinventing the wheel.

If you do want to reinvent the wheel, then the main problem you're having is that you're recreating the captcha when the user submits the form, you have no way to remember and refer to the old value.

So this is how it's working at the moment:

  • User goes to sign in, you generate a captcha, then because they haven't submitted a form, it shows the sign in form including the captcha picture.
  • User fills in the form and hits the submit button- this loads the signup view again,creates a new random captcha, then goes down the form submitted logic path, so when you compare the user captcha data to the current captcha data, it doesn't match.

So you're missing persistence, the captcha you generate the first time round doesn't get held anywhere, so when the user submits you've got no way to refer back to it. So you need to store that captcha word somewhere. You could simply just store that captcha word in the user's session and use that to validate against when you need to, or perhaps sign it with itsdangerous and store it in the form itself as a hidden field?

Code Example:

This just takes your code and adjusts it a little to store the value in the session-- not tested, and can definitely been improved, but should work:

@app.route('/signup', methods = ['GET', 'POST'])
def signup():

    if g.user is not None and g.user.is_authenticated():
        return redirect(url_for('index'))

    if request.method == 'post':
        captcha_word = session["captcha"]
    else:
        words = open('app/corncob_caps.txt').readlines()
        captcha_word = words[random.randint(1,len(words))]
        session["captcha"] = captcha_word
        captcha_filename = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(10)) + '.jpg'
        captcha.gen_captcha(captcha_word.strip(), 'app/os.ttf', 25, 'app/static/' + captcha_filename + '')


    form = SignUpForm(captcha_word)

    if form.validate_on_submit() == False:
        return render_template('signup.html', form = form, filename = captcha_filename)
    else:
        user = User(form.email.data, form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('You have successfully signed up.')
        flash('You may login now.')
        return redirect(url_for('login'))

    return render_template('signup.html', form = form, filename = captcha_filename)

Upvotes: 4

Related Questions