Form rendering, validation, and CSRF protection for Flask with WTForms.
72
Complete Google reCAPTCHA integration for Flask forms, providing bot protection with form fields, server-side validation, and customizable widget rendering. Supports reCAPTCHA v2 with configurable parameters and error handling.
Form field that renders Google reCAPTCHA widget and handles user response validation.
class RecaptchaField:
def __init__(self, label="", validators=None, **kwargs):
"""
reCAPTCHA form field with automatic validation.
Args:
label: Field label (default: empty string)
validators: List of validators (defaults to [Recaptcha()])
**kwargs: Additional field arguments
"""Server-side validator that verifies reCAPTCHA responses with Google's verification service.
class Recaptcha:
def __init__(self, message=None):
"""
reCAPTCHA response validator.
Args:
message: Custom error message for validation failures
"""
def __call__(self, form, field):
"""
Validate reCAPTCHA response against Google's verification API.
Args:
form: Form instance
field: RecaptchaField instance
Raises:
ValidationError: If reCAPTCHA verification fails
"""Widget that renders the Google reCAPTCHA HTML and JavaScript integration.
class RecaptchaWidget:
def recaptcha_html(self, public_key: str) -> Markup:
"""
Generate reCAPTCHA HTML markup.
Args:
public_key: reCAPTCHA site key
Returns:
HTML markup for reCAPTCHA widget
"""
def __call__(self, field, error=None, **kwargs) -> Markup:
"""
Render reCAPTCHA widget for form field.
Args:
field: RecaptchaField instance
error: Validation error (unused)
**kwargs: Additional rendering arguments
Returns:
HTML markup for reCAPTCHA integration
Raises:
RuntimeError: If RECAPTCHA_PUBLIC_KEY is not configured
"""from flask_wtf import FlaskForm
from flask_wtf.recaptcha import RecaptchaField
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email
class ContactForm(FlaskForm):
name = StringField('Name', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
message = TextAreaField('Message', validators=[DataRequired()])
recaptcha = RecaptchaField() # Automatically includes Recaptcha() validator
submit = SubmitField('Send Message')
@app.route('/contact', methods=['GET', 'POST'])
def contact():
form = ContactForm()
if form.validate_on_submit():
# reCAPTCHA validation passed
name = form.name.data
email = form.email.data
message = form.message.data
# Process form (send email, save to database, etc.)
send_contact_email(name, email, message)
flash('Message sent successfully!')
return redirect(url_for('contact'))
return render_template('contact.html', form=form)from flask_wtf.recaptcha import Recaptcha
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
# Custom error message
recaptcha = RecaptchaField(validators=[
Recaptcha(message='Please complete the reCAPTCHA verification')
])
submit = SubmitField('Register')# Different forms can have different reCAPTCHA configurations
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
recaptcha = RecaptchaField()
class CommentForm(FlaskForm):
comment = TextAreaField('Comment', validators=[DataRequired()])
recaptcha = RecaptchaField(label='Verify you are human')class SmartForm(FlaskForm):
content = TextAreaField('Content', validators=[DataRequired()])
submit = SubmitField('Submit')
def __init__(self, require_captcha=False, *args, **kwargs):
super().__init__(*args, **kwargs)
if require_captcha:
# Dynamically add reCAPTCHA field
self.recaptcha = RecaptchaField()
@app.route('/submit', methods=['GET', 'POST'])
def submit_content():
# Require reCAPTCHA for anonymous users
require_captcha = not current_user.is_authenticated
form = SmartForm(require_captcha=require_captcha)
if form.validate_on_submit():
# Process submission
return redirect(url_for('success'))
return render_template('submit.html', form=form)reCAPTCHA requires Google reCAPTCHA keys (obtain from https://www.google.com/recaptcha/):
# Required: reCAPTCHA keys
app.config['RECAPTCHA_PUBLIC_KEY'] = 'your-site-key-here'
app.config['RECAPTCHA_PRIVATE_KEY'] = 'your-secret-key-here'# Custom verification server (default: Google's official server)
app.config['RECAPTCHA_VERIFY_SERVER'] = 'https://www.google.com/recaptcha/api/siteverify'
# reCAPTCHA script parameters (appended as query string)
app.config['RECAPTCHA_PARAMETERS'] = {
'hl': 'en', # Language
'render': 'explicit' # Render mode
}
# Custom reCAPTCHA script URL
app.config['RECAPTCHA_SCRIPT'] = 'https://www.google.com/recaptcha/api.js'
# Custom CSS class for reCAPTCHA div (default: 'g-recaptcha')
app.config['RECAPTCHA_DIV_CLASS'] = 'custom-recaptcha'
# Additional data attributes for reCAPTCHA div
app.config['RECAPTCHA_DATA_ATTRS'] = {
'theme': 'dark',
'size': 'compact',
'callback': 'onRecaptchaSuccess',
'expired-callback': 'onRecaptchaExpired',
'error-callback': 'onRecaptchaError'
}
# Complete custom HTML template (overrides all other settings)
app.config['RECAPTCHA_HTML'] = '''
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<div class="g-recaptcha" data-sitekey="{public_key}" data-theme="dark"></div>
'''<form method="POST">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.name.label(class="form-label") }}
{{ form.name(class="form-control") }}
{% for error in form.name.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<div class="form-group">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% for error in form.email.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<div class="form-group">
{{ form.message.label(class="form-label") }}
{{ form.message(class="form-control", rows="4") }}
{% for error in form.message.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<!-- reCAPTCHA widget -->
<div class="form-group">
{{ form.recaptcha }}
{% for error in form.recaptcha.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
{{ form.submit(class="btn btn-primary") }}
</form><!-- Custom reCAPTCHA container -->
<div class="recaptcha-container">
<div class="recaptcha-label">Please verify you are human:</div>
{{ form.recaptcha }}
{% if form.recaptcha.errors %}
<div class="recaptcha-errors">
{% for error in form.recaptcha.errors %}
<div class="error-message">{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
<style>
.recaptcha-container {
margin: 20px 0;
text-align: center;
}
.recaptcha-label {
margin-bottom: 10px;
font-weight: bold;
}
.recaptcha-errors .error-message {
color: #dc3545;
margin-top: 5px;
}
</style><!-- Custom reCAPTCHA callbacks -->
<script>
function onRecaptchaSuccess(token) {
console.log('reCAPTCHA completed:', token);
// Enable form submission
document.querySelector('button[type="submit"]').disabled = false;
}
function onRecaptchaExpired() {
console.log('reCAPTCHA expired');
// Disable form submission
document.querySelector('button[type="submit"]').disabled = true;
}
function onRecaptchaError() {
console.log('reCAPTCHA error');
alert('reCAPTCHA verification failed. Please try again.');
}
// Initially disable submit button
document.addEventListener('DOMContentLoaded', function() {
document.querySelector('button[type="submit"]').disabled = true;
});
</script>// Submit form with reCAPTCHA via AJAX
function submitForm() {
const form = document.getElementById('contact-form');
const formData = new FormData(form);
fetch('/contact', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Message sent successfully!');
form.reset();
grecaptcha.reset(); // Reset reCAPTCHA
} else {
alert('Error: ' + data.message);
grecaptcha.reset(); // Reset reCAPTCHA on error
}
})
.catch(error => {
console.error('Error:', error);
grecaptcha.reset(); // Reset reCAPTCHA on error
});
}reCAPTCHA validation is automatically disabled during testing:
import unittest
from app import app
class RecaptchaTestCase(unittest.TestCase):
def setUp(self):
self.app = app.test_client()
app.config['TESTING'] = True # Disables reCAPTCHA validation
def test_form_submission_without_recaptcha(self):
# reCAPTCHA validation is skipped in testing mode
response = self.app.post('/contact', data={
'name': 'Test User',
'email': 'test@example.com',
'message': 'Test message',
'csrf_token': get_csrf_token() # Still need CSRF token
})
self.assertEqual(response.status_code, 302) # Successful redirect@app.errorhandler(ValidationError)
def handle_validation_error(e):
if 'recaptcha' in str(e).lower():
# Handle reCAPTCHA-specific errors
return render_template('recaptcha_error.html'), 400
return render_template('error.html'), 400
# Custom error messages based on reCAPTCHA response
RECAPTCHA_ERROR_MESSAGES = {
'missing-input-secret': 'reCAPTCHA configuration error',
'invalid-input-secret': 'reCAPTCHA configuration error',
'missing-input-response': 'Please complete the reCAPTCHA verification',
'invalid-input-response': 'reCAPTCHA verification failed',
'incorrect-captcha-sol': 'reCAPTCHA verification failed'
}# Handle reCAPTCHA API timeouts and network errors
from urllib.error import URLError, HTTPError
import logging
class RobustRecaptcha(Recaptcha):
def _validate_recaptcha(self, response, remote_addr):
try:
return super()._validate_recaptcha(response, remote_addr)
except (URLError, HTTPError) as e:
logging.error(f'reCAPTCHA API error: {e}')
# In production, you might want to allow submission
# when reCAPTCHA service is unavailable
if app.config.get('RECAPTCHA_FALLBACK_ON_ERROR'):
return True
raise ValidationError('reCAPTCHA service temporarily unavailable')Install with Tessl CLI
npx tessl i tessl/pypi-flask-wtfevals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10