0
# reCAPTCHA Integration
1
2
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.
3
4
## Capabilities
5
6
### reCAPTCHA Field
7
8
Form field that renders Google reCAPTCHA widget and handles user response validation.
9
10
```python { .api }
11
class RecaptchaField:
12
def __init__(self, label="", validators=None, **kwargs):
13
"""
14
reCAPTCHA form field with automatic validation.
15
16
Args:
17
label: Field label (default: empty string)
18
validators: List of validators (defaults to [Recaptcha()])
19
**kwargs: Additional field arguments
20
"""
21
```
22
23
### reCAPTCHA Validator
24
25
Server-side validator that verifies reCAPTCHA responses with Google's verification service.
26
27
```python { .api }
28
class Recaptcha:
29
def __init__(self, message=None):
30
"""
31
reCAPTCHA response validator.
32
33
Args:
34
message: Custom error message for validation failures
35
"""
36
37
def __call__(self, form, field):
38
"""
39
Validate reCAPTCHA response against Google's verification API.
40
41
Args:
42
form: Form instance
43
field: RecaptchaField instance
44
45
Raises:
46
ValidationError: If reCAPTCHA verification fails
47
"""
48
```
49
50
### reCAPTCHA Widget
51
52
Widget that renders the Google reCAPTCHA HTML and JavaScript integration.
53
54
```python { .api }
55
class RecaptchaWidget:
56
def recaptcha_html(self, public_key: str) -> Markup:
57
"""
58
Generate reCAPTCHA HTML markup.
59
60
Args:
61
public_key: reCAPTCHA site key
62
63
Returns:
64
HTML markup for reCAPTCHA widget
65
"""
66
67
def __call__(self, field, error=None, **kwargs) -> Markup:
68
"""
69
Render reCAPTCHA widget for form field.
70
71
Args:
72
field: RecaptchaField instance
73
error: Validation error (unused)
74
**kwargs: Additional rendering arguments
75
76
Returns:
77
HTML markup for reCAPTCHA integration
78
79
Raises:
80
RuntimeError: If RECAPTCHA_PUBLIC_KEY is not configured
81
"""
82
```
83
84
## Usage Examples
85
86
### Basic reCAPTCHA Integration
87
88
```python
89
from flask_wtf import FlaskForm
90
from flask_wtf.recaptcha import RecaptchaField
91
from wtforms import StringField, TextAreaField, SubmitField
92
from wtforms.validators import DataRequired, Email
93
94
class ContactForm(FlaskForm):
95
name = StringField('Name', validators=[DataRequired()])
96
email = StringField('Email', validators=[DataRequired(), Email()])
97
message = TextAreaField('Message', validators=[DataRequired()])
98
recaptcha = RecaptchaField() # Automatically includes Recaptcha() validator
99
submit = SubmitField('Send Message')
100
101
@app.route('/contact', methods=['GET', 'POST'])
102
def contact():
103
form = ContactForm()
104
105
if form.validate_on_submit():
106
# reCAPTCHA validation passed
107
name = form.name.data
108
email = form.email.data
109
message = form.message.data
110
111
# Process form (send email, save to database, etc.)
112
send_contact_email(name, email, message)
113
flash('Message sent successfully!')
114
return redirect(url_for('contact'))
115
116
return render_template('contact.html', form=form)
117
```
118
119
### Custom reCAPTCHA Validation
120
121
```python
122
from flask_wtf.recaptcha import Recaptcha
123
124
class RegistrationForm(FlaskForm):
125
username = StringField('Username', validators=[DataRequired()])
126
email = StringField('Email', validators=[DataRequired(), Email()])
127
password = PasswordField('Password', validators=[DataRequired()])
128
129
# Custom error message
130
recaptcha = RecaptchaField(validators=[
131
Recaptcha(message='Please complete the reCAPTCHA verification')
132
])
133
134
submit = SubmitField('Register')
135
```
136
137
### Multiple reCAPTCHA Forms
138
139
```python
140
# Different forms can have different reCAPTCHA configurations
141
class LoginForm(FlaskForm):
142
username = StringField('Username', validators=[DataRequired()])
143
password = PasswordField('Password', validators=[DataRequired()])
144
recaptcha = RecaptchaField()
145
146
class CommentForm(FlaskForm):
147
comment = TextAreaField('Comment', validators=[DataRequired()])
148
recaptcha = RecaptchaField(label='Verify you are human')
149
```
150
151
### Conditional reCAPTCHA
152
153
```python
154
class SmartForm(FlaskForm):
155
content = TextAreaField('Content', validators=[DataRequired()])
156
submit = SubmitField('Submit')
157
158
def __init__(self, require_captcha=False, *args, **kwargs):
159
super().__init__(*args, **kwargs)
160
161
if require_captcha:
162
# Dynamically add reCAPTCHA field
163
self.recaptcha = RecaptchaField()
164
165
@app.route('/submit', methods=['GET', 'POST'])
166
def submit_content():
167
# Require reCAPTCHA for anonymous users
168
require_captcha = not current_user.is_authenticated
169
form = SmartForm(require_captcha=require_captcha)
170
171
if form.validate_on_submit():
172
# Process submission
173
return redirect(url_for('success'))
174
175
return render_template('submit.html', form=form)
176
```
177
178
## Configuration
179
180
### Required Configuration
181
182
reCAPTCHA requires Google reCAPTCHA keys (obtain from https://www.google.com/recaptcha/):
183
184
```python
185
# Required: reCAPTCHA keys
186
app.config['RECAPTCHA_PUBLIC_KEY'] = 'your-site-key-here'
187
app.config['RECAPTCHA_PRIVATE_KEY'] = 'your-secret-key-here'
188
```
189
190
### Optional Configuration
191
192
```python
193
# Custom verification server (default: Google's official server)
194
app.config['RECAPTCHA_VERIFY_SERVER'] = 'https://www.google.com/recaptcha/api/siteverify'
195
196
# reCAPTCHA script parameters (appended as query string)
197
app.config['RECAPTCHA_PARAMETERS'] = {
198
'hl': 'en', # Language
199
'render': 'explicit' # Render mode
200
}
201
202
# Custom reCAPTCHA script URL
203
app.config['RECAPTCHA_SCRIPT'] = 'https://www.google.com/recaptcha/api.js'
204
205
# Custom CSS class for reCAPTCHA div (default: 'g-recaptcha')
206
app.config['RECAPTCHA_DIV_CLASS'] = 'custom-recaptcha'
207
208
# Additional data attributes for reCAPTCHA div
209
app.config['RECAPTCHA_DATA_ATTRS'] = {
210
'theme': 'dark',
211
'size': 'compact',
212
'callback': 'onRecaptchaSuccess',
213
'expired-callback': 'onRecaptchaExpired',
214
'error-callback': 'onRecaptchaError'
215
}
216
217
# Complete custom HTML template (overrides all other settings)
218
app.config['RECAPTCHA_HTML'] = '''
219
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
220
<div class="g-recaptcha" data-sitekey="{public_key}" data-theme="dark"></div>
221
'''
222
```
223
224
## Template Integration
225
226
### Basic Template
227
228
```html
229
<form method="POST">
230
{{ form.hidden_tag() }}
231
232
<div class="form-group">
233
{{ form.name.label(class="form-label") }}
234
{{ form.name(class="form-control") }}
235
{% for error in form.name.errors %}
236
<div class="text-danger">{{ error }}</div>
237
{% endfor %}
238
</div>
239
240
<div class="form-group">
241
{{ form.email.label(class="form-label") }}
242
{{ form.email(class="form-control") }}
243
{% for error in form.email.errors %}
244
<div class="text-danger">{{ error }}</div>
245
{% endfor %}
246
</div>
247
248
<div class="form-group">
249
{{ form.message.label(class="form-label") }}
250
{{ form.message(class="form-control", rows="4") }}
251
{% for error in form.message.errors %}
252
<div class="text-danger">{{ error }}</div>
253
{% endfor %}
254
</div>
255
256
<!-- reCAPTCHA widget -->
257
<div class="form-group">
258
{{ form.recaptcha }}
259
{% for error in form.recaptcha.errors %}
260
<div class="text-danger">{{ error }}</div>
261
{% endfor %}
262
</div>
263
264
{{ form.submit(class="btn btn-primary") }}
265
</form>
266
```
267
268
### Custom Styling
269
270
```html
271
<!-- Custom reCAPTCHA container -->
272
<div class="recaptcha-container">
273
<div class="recaptcha-label">Please verify you are human:</div>
274
{{ form.recaptcha }}
275
{% if form.recaptcha.errors %}
276
<div class="recaptcha-errors">
277
{% for error in form.recaptcha.errors %}
278
<div class="error-message">{{ error }}</div>
279
{% endfor %}
280
</div>
281
{% endif %}
282
</div>
283
284
<style>
285
.recaptcha-container {
286
margin: 20px 0;
287
text-align: center;
288
}
289
290
.recaptcha-label {
291
margin-bottom: 10px;
292
font-weight: bold;
293
}
294
295
.recaptcha-errors .error-message {
296
color: #dc3545;
297
margin-top: 5px;
298
}
299
</style>
300
```
301
302
## Advanced Usage
303
304
### JavaScript Integration
305
306
```html
307
<!-- Custom reCAPTCHA callbacks -->
308
<script>
309
function onRecaptchaSuccess(token) {
310
console.log('reCAPTCHA completed:', token);
311
// Enable form submission
312
document.querySelector('button[type="submit"]').disabled = false;
313
}
314
315
function onRecaptchaExpired() {
316
console.log('reCAPTCHA expired');
317
// Disable form submission
318
document.querySelector('button[type="submit"]').disabled = true;
319
}
320
321
function onRecaptchaError() {
322
console.log('reCAPTCHA error');
323
alert('reCAPTCHA verification failed. Please try again.');
324
}
325
326
// Initially disable submit button
327
document.addEventListener('DOMContentLoaded', function() {
328
document.querySelector('button[type="submit"]').disabled = true;
329
});
330
</script>
331
```
332
333
### AJAX Forms with reCAPTCHA
334
335
```javascript
336
// Submit form with reCAPTCHA via AJAX
337
function submitForm() {
338
const form = document.getElementById('contact-form');
339
const formData = new FormData(form);
340
341
fetch('/contact', {
342
method: 'POST',
343
body: formData
344
})
345
.then(response => response.json())
346
.then(data => {
347
if (data.success) {
348
alert('Message sent successfully!');
349
form.reset();
350
grecaptcha.reset(); // Reset reCAPTCHA
351
} else {
352
alert('Error: ' + data.message);
353
grecaptcha.reset(); // Reset reCAPTCHA on error
354
}
355
})
356
.catch(error => {
357
console.error('Error:', error);
358
grecaptcha.reset(); // Reset reCAPTCHA on error
359
});
360
}
361
```
362
363
### Testing
364
365
reCAPTCHA validation is automatically disabled during testing:
366
367
```python
368
import unittest
369
from app import app
370
371
class RecaptchaTestCase(unittest.TestCase):
372
def setUp(self):
373
self.app = app.test_client()
374
app.config['TESTING'] = True # Disables reCAPTCHA validation
375
376
def test_form_submission_without_recaptcha(self):
377
# reCAPTCHA validation is skipped in testing mode
378
response = self.app.post('/contact', data={
379
'name': 'Test User',
380
'email': 'test@example.com',
381
'message': 'Test message',
382
'csrf_token': get_csrf_token() # Still need CSRF token
383
})
384
385
self.assertEqual(response.status_code, 302) # Successful redirect
386
```
387
388
## Error Handling
389
390
### Common Error Scenarios
391
392
```python
393
@app.errorhandler(ValidationError)
394
def handle_validation_error(e):
395
if 'recaptcha' in str(e).lower():
396
# Handle reCAPTCHA-specific errors
397
return render_template('recaptcha_error.html'), 400
398
399
return render_template('error.html'), 400
400
401
# Custom error messages based on reCAPTCHA response
402
RECAPTCHA_ERROR_MESSAGES = {
403
'missing-input-secret': 'reCAPTCHA configuration error',
404
'invalid-input-secret': 'reCAPTCHA configuration error',
405
'missing-input-response': 'Please complete the reCAPTCHA verification',
406
'invalid-input-response': 'reCAPTCHA verification failed',
407
'incorrect-captcha-sol': 'reCAPTCHA verification failed'
408
}
409
```
410
411
### Network and API Issues
412
413
```python
414
# Handle reCAPTCHA API timeouts and network errors
415
from urllib.error import URLError, HTTPError
416
import logging
417
418
class RobustRecaptcha(Recaptcha):
419
def _validate_recaptcha(self, response, remote_addr):
420
try:
421
return super()._validate_recaptcha(response, remote_addr)
422
except (URLError, HTTPError) as e:
423
logging.error(f'reCAPTCHA API error: {e}')
424
# In production, you might want to allow submission
425
# when reCAPTCHA service is unavailable
426
if app.config.get('RECAPTCHA_FALLBACK_ON_ERROR'):
427
return True
428
raise ValidationError('reCAPTCHA service temporarily unavailable')
429
```