or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

csrf-protection.mdfile-upload.mdform-handling.mdindex.mdrecaptcha.md

recaptcha.mddocs/

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

```