feat(security): add Cloudflare Turnstile CAPTCHA to registration form

- Add Turnstile widget rendering in RegisterPage on step 2
- Implement programmatic widget initialization with callbacks
- Add token validation before form submission
- Update AuthContext and API service to pass turnstileToken
- Add backend verification via Cloudflare API in register controller
- Include client IP in verification request
- Add validation rule for turnstileToken
- Reset widget on registration error
This commit is contained in:
Radosław Gierwiało
2025-12-05 18:20:26 +01:00
parent f3b8156557
commit d8085f828f
6 changed files with 112 additions and 10 deletions

View File

@@ -16,7 +16,38 @@ const { getClientIP } = require('../utils/request');
// Register new user (Phase 1.5 - with WSDC support and email verification)
async function register(req, res, next) {
try {
const { username, email, password, firstName, lastName, wsdcId } = req.body;
const { username, email, password, firstName, lastName, wsdcId, turnstileToken } = req.body;
// Verify Turnstile token
const turnstileSecret = process.env.TURNSTILE_SECRET_KEY;
const turnstileVerifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
try {
const turnstileResponse = await fetch(turnstileVerifyUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: turnstileSecret,
response: turnstileToken,
remoteip: getClientIP(req),
}),
});
const turnstileResult = await turnstileResponse.json();
if (!turnstileResult.success) {
return res.status(400).json({
success: false,
error: 'CAPTCHA verification failed. Please try again.',
});
}
} catch (turnstileError) {
console.error('Turnstile verification error:', turnstileError);
return res.status(500).json({
success: false,
error: 'CAPTCHA verification failed. Please try again.',
});
}
// Check if user already exists
const existingUser = await prisma.user.findFirst({

View File

@@ -74,6 +74,9 @@ const registerValidation = [
.trim()
.matches(/^\d{1,10}$/)
.withMessage('WSDC ID must be numeric (max 10 digits)'),
body('turnstileToken')
.notEmpty()
.withMessage('CAPTCHA verification is required'),
handleValidationErrors,
];

View File

@@ -79,8 +79,6 @@ router.post('/contact', [
const turnstileSecret = process.env.TURNSTILE_SECRET_KEY;
const turnstileVerifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
console.log('[Turnstile] Verifying token, secret present:', !!turnstileSecret);
try {
const turnstileResponse = await fetch(turnstileVerifyUrl, {
method: 'POST',
@@ -93,7 +91,6 @@ router.post('/contact', [
});
const turnstileResult = await turnstileResponse.json();
console.log('[Turnstile] Verification result:', JSON.stringify(turnstileResult));
if (!turnstileResult.success) {
return res.status(400).json({