CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-flask-wtf

Form rendering, validation, and CSRF protection for Flask with WTForms.

72

0.91x
Overview
Eval results
Files

recaptcha.mddocs/

reCAPTCHA Integration

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.

Capabilities

reCAPTCHA Field

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
        """

reCAPTCHA Validator

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
        """

reCAPTCHA Widget

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
        """

Usage Examples

Basic reCAPTCHA Integration

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)

Custom reCAPTCHA Validation

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')

Multiple reCAPTCHA Forms

# 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')

Conditional reCAPTCHA

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)

Configuration

Required Configuration

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'

Optional Configuration

# 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>
'''

Template Integration

Basic Template

<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 Styling

<!-- 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>

Advanced Usage

JavaScript Integration

<!-- 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>

AJAX Forms with reCAPTCHA

// 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
    });
}

Testing

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

Error Handling

Common Error Scenarios

@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'
}

Network and API Issues

# 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-wtf

docs

csrf-protection.md

file-upload.md

form-handling.md

index.md

recaptcha.md

tile.json