Skip to content

Commit 4a264d1

Browse files
server-side validation
1 parent 810dbc1 commit 4a264d1

File tree

1 file changed

+180
-0
lines changed

1 file changed

+180
-0
lines changed

src/content/docs/turnstile/get-started/server-side-validation.mdx

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,3 +486,183 @@ if (result.valid) {
486486
console.log('Validation failed:', result.reason);
487487
}
488488
```
489+
490+
---
491+
492+
## API response format
493+
494+
<Tabs>
495+
<TabItem label="Successful response">
496+
```json title="Example"
497+
{
498+
"success": true,
499+
"challenge_ts": "2022-02-28T15:14:30.096Z",
500+
"hostname": "example.com",
501+
"error-codes": [],
502+
"action": "login",
503+
"cdata": "sessionid-123456789",
504+
"metadata": {
505+
"ephemeral_id": "x:9f78e0ed210960d7693b167e"
506+
}
507+
}
508+
```
509+
</TabItem>
510+
<TabItem label="Failed response">
511+
```json title="Example"
512+
{
513+
"success": false,
514+
"error-codes": ["invalid-input-response"]
515+
}
516+
```
517+
</TabItem>
518+
</Tabs>
519+
520+
### Response fields
521+
522+
| Field | Description |
523+
| --- | --- |
524+
| `success` | Boolean indicating if validation was successful |
525+
| `challenge_ts` | ISO timestamp when the challenge was solved |
526+
| `hostname` | Hostname where the challenge was served |
527+
| `error-codes` | Array of error codes (if validation failed) |
528+
| `action` | Custom action identifier from client-side |
529+
| `cdata` | Custom data payload from client-side |
530+
| `metadata.ephemeral_id` | Device fingerprint ID (Enterprise only) |
531+
532+
### Error codes reference
533+
534+
| Error code | Description | Action required |
535+
| --- | --- | --- |
536+
| `missing-input-secret` | Secret parameter not provided | Ensure secret key is included |
537+
| `invalid-input-secret` | Secret key is invalid or expired | Check your secret key in the Cloudflare dashboard |
538+
| `missing-input-response` | Response parameter was not provided | Ensure token is included |
539+
| `invalid-input-response` | Token is invalid, malformed, or expired | User should retry the challenge |
540+
| `bad-request` | Request is malformed | Check request format and parameters |
541+
| `timeout-or-duplicate` | Token has already been validated | Each token can only be used once |
542+
| `internal-error` | Internal error occurred | Retry the request |
543+
544+
---
545+
546+
## Implementation
547+
548+
``` js title="Example implementation"
549+
class TurnstileValidator {
550+
constructor(secretKey, timeout = 10000) {
551+
this.secretKey = secretKey;
552+
this.timeout = timeout;
553+
}
554+
555+
async validate(token, remoteip, options = {}) {
556+
// Input validation
557+
if (!token || typeof token !== 'string') {
558+
return { success: false, error: 'Invalid token format' };
559+
}
560+
561+
if (token.length > 2048) {
562+
return { success: false, error: 'Token too long' };
563+
}
564+
565+
// Prepare request
566+
const controller = new AbortController();
567+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
568+
569+
try {
570+
const formData = new FormData();
571+
formData.append('secret', this.secretKey);
572+
formData.append('response', token);
573+
574+
if (remoteip) {
575+
formData.append('remoteip', remoteip);
576+
}
577+
578+
if (options.idempotencyKey) {
579+
formData.append('idempotency_key', options.idempotencyKey);
580+
}
581+
582+
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
583+
method: 'POST',
584+
body: formData,
585+
signal: controller.signal
586+
});
587+
588+
const result = await response.json();
589+
590+
// Additional validation
591+
if (result.success) {
592+
if (options.expectedAction && result.action !== options.expectedAction) {
593+
return {
594+
success: false,
595+
error: 'Action mismatch',
596+
expected: options.expectedAction,
597+
received: result.action
598+
};
599+
}
600+
601+
if (options.expectedHostname && result.hostname !== options.expectedHostname) {
602+
return {
603+
success: false,
604+
error: 'Hostname mismatch',
605+
expected: options.expectedHostname,
606+
received: result.hostname
607+
};
608+
}
609+
}
610+
611+
return result;
612+
613+
} catch (error) {
614+
if (error.name === 'AbortError') {
615+
return { success: false, error: 'Validation timeout' };
616+
}
617+
618+
console.error('Turnstile validation error:', error);
619+
return { success: false, error: 'Internal error' };
620+
621+
} finally {
622+
clearTimeout(timeoutId);
623+
}
624+
}
625+
}
626+
627+
// Usage
628+
const validator = new TurnstileValidator(process.env.TURNSTILE_SECRET_KEY);
629+
630+
const result = await validator.validate(token, remoteip, {
631+
expectedAction: 'login',
632+
expectedHostname: 'example.com'
633+
});
634+
635+
if (result.success) {
636+
// Process the request
637+
} else {
638+
// Handle failure
639+
console.log('Validation failed:', result.error);
640+
}
641+
642+
```
643+
644+
---
645+
646+
## Best practices
647+
648+
### Security
649+
650+
- Store secret keys securely. Use environment variables or secure key management.
651+
- Validate the token on every request. Never trust client-side validation alone.
652+
- Check additional fields. Validate the action and hostname when specified.
653+
- Monitor for abuse. Log failed validations and unusual patterns.
654+
- Use HTTPS. Always validate over secure connections.
655+
656+
### Performance
657+
658+
- Set reasonable timeouts. Do not wait indefinitely for Siteverify responses.
659+
- Implement retry logic.
660+
- Cache validation results for the same token, if needed for your flow.
661+
- Monitor API latency. Track Siteverify response times.
662+
663+
### Error handling
664+
665+
- Have fallback behavior for API failures.
666+
- Do not expose internal error details to users.
667+
- Properly log errors for debugging without exposing secrets.
668+
- Rate limit. Protect against validation flooding.

0 commit comments

Comments
 (0)