angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Creating a Captcha with Flask, WTForms, SQLAlchemy, SQLite

Respect the privacy of your visitors, do not connect them to a third-party Captcha service!

10 April 2023 Updated 10 April 2023
post main image

In the past I wrote some code for a (text-only) Captcha for a Flask website. This is an update. Here I use a Pypi package to generate the image. Besides that I also added a Captcha image refresh button.
You can try the code below, it is a (newsletter) subscription form. Although this is a Captcha solution for a Flask website, it can be converted into a Captcha server.

Note that I use SQLite here for demonstration purpose only. Do not use SQLite in a multiuser environment!

Description

Our application has a (newsletter) subscribe-form, where the visitor can type his email address. This form is protected by a Captcha.

Every time the form is displayed, the Captcha data (token, code, image) is generated and stored in a database. When the form is submitted, we use the token to retrieve the Captcha data, and compare the code typed by the visitor against the code stored in the database.

In addition, there is a 'refresh' button next to the image. When clicked, a new image is shown.

On the subscribe HTML page we also use:

  • Bootstrap
  • JQuery

The Captcha refresh button uses a Bootstrap icon.

The CaptchaField

Requirements for our new CaptchaField:

  • A unique token, in a hidden field
  • An img-tag that pulls the image from the server.
  • A text input field where the code can be typed.
  • Custom validation to check the code typed.

The main difficulty here is the Captcha token, we must store this also in the field.

Because we need a text input function to enter the Captcha code, we use the StringField code from WTForms as a basis for our CaptchaField.

class StringField(Field):
    """
    This field is the base for most of the more complicated fields, and
    represents an ``<input type="text">``.
    """

    widget = widgets.TextInput()

    def process_formdata(self, valuelist):
        if valuelist:
            self.data = valuelist[0]

    def _value(self):
        return str(self.data) if self.data is not None else ""

We override the methods:

  • __call__
  • post_validate

In our new '__call__' method, we clear the previously entered code, generate a new a Captcha and then add extra HTML:

  • A hidden field to store the token.
  • An img-tag to pull the image
  • A button to refresh the image

In our new 'post_validate' method, we:

  • Retrieve the token and code from the request.
  • Lookup the database Captcha record using the token.
  • Compare the code with the stored code.

We create a database table 'Captcha' with the following fields:

  • token
  • code
  • image data

I created a class CaptchaUtils() to create a Captcha and validate a Captcha.

Once the above is done, the only thing we have to do when we want to add a Captcha to a form:

class SubscribeForm(wtf.FlaskForm):
    ...
    captcha_code = CaptchaField(
		'Enter code',
        validators=[wtv.InputRequired()]
    )
    ...

To refresh the Captcha, we call the Flask server to generate a new Captcha. This call returns the new:

  • captcha_token
  • captcha_image_url

On the subscribe page we can do this with a few lines of Javascript (JQuery):

$(document).ready(function(){
	$('#captcha-refresh-button').click(function(){
		captcha_new_url = '/captcha/new';
		$.getJSON(captcha_new_url, function(data){
			$('#captcha-token').val(data.captcha_token);
			$('#captcha-image').attr('src', data.captcha_image_url);
		});
	});
});

The code

As always, we create a virtual environment. Install the packages:

pip install flask
pip install flask-wtf
pip install sqlalchemy
pip install captcha

Next, we create the 'project' directory, etc. Here is the directory structure:

.
├── project
│   ├── app
│   │   ├── factory.py
│   │   └── templates
│   │       └── subscribe.html
│   └── run.py

And here are the files in the project, the SQLite database file will appear in the project directory:

  • run.py
  • factory.py
  • subscribe.html
# run.py
from app.factory import create_app

app = create_app('development')

if __name__ == '__main__':
    app.run(
        host= '127.0.0.1',
        port=5555,
        debug=True,
        use_reloader=True,
    )
# factory.py
import datetime
import io
import logging
import os
import random
import sys
import uuid

from flask import current_app, Flask, jsonify, make_response, render_template, request, url_for

import markupsafe

import sqlalchemy as sa
import sqlalchemy.orm as orm

import flask_wtf as wtf
import wtforms as wt
import wtforms.validators as wtv
import wtforms.widgets as wtw

from captcha.image import ImageCaptcha


class Base(orm.DeclarativeBase):
    pass

class Captcha(Base):
    __tablename__ = 'captcha'

    id = sa.Column(sa.Integer, primary_key=True)
    created_on = sa.Column(sa.DateTime, server_default=sa.func.now())
    token = sa.Column(sa.String(40), index=True)
    code = sa.Column(sa.String(10))
    image = sa.Column(sa.LargeBinary)

    def __str__(self):
        return '<Captcha: id = {}, token = {}, code = {}>'.format(id, token, code)
    
# create table
engine = sa.create_engine('sqlite:///flask_captcha.sqlite', echo=True)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)

# get scoped session for web
db_session = orm.scoped_session(orm.sessionmaker(
        autocommit=False,
        autoflush=False,
        bind=engine
    )
)


class CaptchaUtils:
    code_chars = ['A', 'b', 'C', 'd', 'e', 'f', 'h', 'K', 'M', 'N', 'p', 'R', 'S', 'T', 'W', 'X', 'Y', 'Z']
    code_nums = ['2', '3', '4', '6', '7', '8', '9']
        
    @classmethod
    def get_random_code(cls, n):
       return ''.join(random.choice(cls.code_chars + cls.code_nums) for i in range(n))

    @classmethod
    def create(cls, db):
        # 1. create captcha token 
        captcha_token = uuid.uuid4().hex
        # 2. create captcha code 
        captcha_code = cls.get_random_code(5)
        # 3. create captcha image 
        im = ImageCaptcha()
        im_buf = io.BytesIO()
        im.write(captcha_code, im_buf, format='png')
        captcha_image = im_buf.getvalue()
        # 4. store in db
        captcha = Captcha(
            token=captcha_token,
            code=captcha_code,
            image=captcha_image
        )
        db.add(captcha)
        db.commit()
        return captcha_token, url_for('captcha_image', token=captcha_token)

    @classmethod
    def validate(cls, db, token, code):
        stmt = sa.select(Captcha).where(Captcha.token == token)
        captcha = db_session.execute(stmt).scalars().first()
        if captcha is None:
            return False
        if code.lower() != captcha.code.lower():
            return False
        return True


class CaptchaField(wt.Field):
    widget = wtw.TextInput()

    def process_formdata(self, valuelist):
        if valuelist:
            self.data = valuelist[0]

    def _value(self):
        return str(self.data) if self.data is not None else ""

    def  __call__(self, *args, **kwargs):
        self.data = ''
        input_field_html = super(CaptchaField, self).__call__(*args,**kwargs)

        captcha_token, captcha_image_url = CaptchaUtils.create(db_session)

        hidden_field_html = markupsafe.Markup('<input type="hidden" name="captcha_token" value="{}" id="captcha-token">'.format(captcha_token))
        image_html = markupsafe.Markup('<img src="{}" class="border mb-2" id="captcha-image">'.format(captcha_image_url))
        button_html = markupsafe.Markup("""<button type="button" class="btn btn-outline-secondary btn-sm ms-1" id="captcha-refresh-button">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg></button>""")
        break_html = markupsafe.Markup('<br>')
        return hidden_field_html + image_html + button_html + break_html + input_field_html

    def post_validate(self, form, validation_stopped):
        if not CaptchaUtils.validate(
            db_session,
            request.form.get('captcha_token', None),
            form.captcha_code.data
        ):
            raise wtv.ValidationError('Captcha code invalid')


class SubscribeForm(wtf.FlaskForm):
    email = wt.StringField(
        'Email',
        validators=[wtv.InputRequired(), wtv.Length(min=4, max=100)]
    )
    captcha_code = CaptchaField(
        'Enter code',
        validators=[wtv.InputRequired()]
    )


def create_app(deploy_config):
    app = Flask(__name__)
    app.config['TEMPLATES_AUTO_RELOAD'] = True
    app.config.update(
        SECRET_KEY='your-secret-key',
        DEPLOY_CONFIG=deploy_config,
    )
    app.logger.setLevel(logging.DEBUG)

    # routes
    @app.route('/')
    def index():
        return 'Home'

    @app.route('/subscribe', methods=('GET', 'POST'))
    def subscribe():
        form = SubscribeForm()
        if form.validate_on_submit():
            return 'Thank you'
        return render_template(
            'subscribe.html', 
            form=form,
        )

    @app.route('/captcha/image/<token>', methods=('GET', 'POST'))
    def captcha_image(token):
        stmt = sa.select(Captcha).where(Captcha.token == token)
        captcha = db_session.execute(stmt).scalars().first()
        if captcha is None:
            return ''
        response = make_response(captcha.image)
        response.headers.set('Content-Type', 'image/png')
        return response

    @app.route('/captcha/new', methods=('GET', 'POST'))
    def captcha_new():
        captcha_token, captcha_image_url = CaptchaUtils.create(db_session)
        return jsonify({
            'captcha_token': captcha_token,
            'captcha_image_url': captcha_image_url
        })

    return app
<!DOCTYPE html>
<html lang = "en">
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>
<body>

<div class="container">
	<h3 class="my-3">
		Subscribe to our newsletter
	</h3>
      
	<form method="post">
		{{ form.csrf_token }}

		<div class="row mt-2">
			<div class="col-2">
				{{ form.email.label }}
			</div>
			<div class="col-10">
				{{ form.email() }}
				{% if form.email.errors %}
					<ul class="text-danger">
					{% for error in form.email.errors %}
						<li>{{ error }}</li>
					{% endfor %}
					</ul>
				{% endif %}
			</div>
		</div>	

		<div class="row mt-2">
			<div class="col-2">
				{{ form.captcha_code.label }}
			</div>
			<div class="col-10">
				{{ form.captcha_code() }}
				{% if form.captcha_code.errors %}
					<ul class="text-danger">
					{% for error in form.captcha_code.errors %}
						<li>{{ error }}</li>
					{% endfor %}
					</ul>
				{% endif %}
			</div>
		</div>	

		<div class="row mt-2">
			<div class="col-2"></div>
			<div class="col-10">
				<input type="submit" value="Submit" class="btn btn-primary">
			</div>
		</div>
	</form>

</div>      

<script src="https://code.jquery.com/jquery-3.6.4.min.js" integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8=" crossorigin="anonymous"></script>
<script>
$(document).ready(function(){
	$('#captcha-refresh-button').click(function(){
		captcha_new_url = '/captcha/new';
		$.getJSON(captcha_new_url, function(data){
			$('#captcha-token').val(data.captcha_token);
			$('#captcha-image').attr('src', data.captcha_image_url);
		});
	});
});
</script>
		
</body>
</html>

Summary

A Captcha is still very useful today. Here we used a Pypi package to generate the image. Although the 'blurring' looks good, it is more vulnerable to machine attacks because of the open source code. You probably can do better with your own algorithm. And by using your own served Captcha instead of a third-party Captcha service, you protect the privacy of your visitors!

Links / credits

Another captcha implementation for Flask and WTForms
https://www.peterspython.com/en/blog/another-captcha-implementation-for-flask-and-wtforms

Captcha
https://pypi.org/project/captcha

DB Browser for SQLite
https://sqlitebrowser.org

WTForms
https://wtforms.readthedocs.io/en/3.0.x/