Creating a Captcha with Flask, WTForms, SQLAlchemy, SQLite
Respect the privacy of your visitors, do not connect them to a third-party Captcha service!
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/
Read more
Captcha Flask SQLAlchemy WTForms
Most viewed
- Using Python's pyOpenSSL to verify SSL certificates downloaded from a host
- Using PyInstaller and Cython to create a Python executable
- Reducing page response times of a Flask SQLAlchemy website
- Connect to a service on a Docker host from a Docker container
- Using UUIDs instead of Integer Autoincrement Primary Keys with SQLAlchemy and MariaDb
- SQLAlchemy: Using Cascade Deletes to delete related objects