Skip to content

Conversation

@gjeanmart
Copy link
Contributor

Summary

Adds Cloudflare Turnstile CAPTCHA verification to protect sensitive API endpoints from bot abuse.

  • Implement CaptchaModule with CaptchaService for server-side token verification via Turnstile API
  • Add CaptchaGuard that validates X-Captcha-Token header on protected routes
  • Enable CAPTCHA protection on the owners route (GET /v2/owners/:ownerAddress/safes)
  • Add configuration options to enable/disable CAPTCHA and configure the secret key

Changes

File Description
src/routes/captcha/captcha.module.ts New module exporting CaptchaService and CaptchaGuard
src/routes/captcha/captcha.service.ts Service to verify Turnstile tokens via Cloudflare API
src/routes/captcha/guards/captcha.guard.ts NestJS guard that validates X-Captcha-Token header
src/modules/owners/routes/owners.controller.v2.ts Apply CaptchaGuard to getAllSafesByOwner endpoint
src/modules/owners/owners.module.ts Import CaptchaModule
src/config/entities/configuration.ts Add captcha configuration (enabled, secretKey)
.env.sample Add CAPTCHA_ENABLED and CAPTCHA_SECRET_KEY variables

Configuration

Environment Variable Description Default
CAPTCHA_ENABLED Enable/disable CAPTCHA verification false
CAPTCHA_SECRET_KEY Cloudflare Turnstile secret key -

How it works

  1. Client includes X-Captcha-Token header with Turnstile token
  2. CaptchaGuard intercepts the request and validates the token
  3. Token is verified server-side via Cloudflare's siteverify API
  4. Request proceeds if valid, returns 401 Unauthorized if invalid

Test plan

  • Test with CAPTCHA disabled (CAPTCHA_ENABLED=false) - requests should pass without token
  • Test with CAPTCHA enabled and valid token - request should succeed
  • Test with CAPTCHA enabled and missing token - should return 401
  • Test with CAPTCHA enabled and invalid token - should return 401

…dpoints

- Add CaptchaModule with Turnstile token verification service
- Add CaptchaGuard to protect sensitive endpoints (e.g., owners controller)
- Add Turnstile configuration (secret key) to application config
- Update .env.sample with Turnstile environment variables
@gjeanmart gjeanmart self-assigned this Jan 30, 2026
throw new UnauthorizedException('CAPTCHA token is required');
}

const remoteip =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit(recommendation): We could extract this to a separate helper for reusability as we are fetching the client IP in different places.

} from '@/datasources/network/network.service.interface';
import { ILoggingService, LoggingService } from '@/logging/logging.interface';

interface TurnstileVerifyResponse {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should extract it to a separate interface/entity file.

});

// response.data is Raw<TurnstileVerifyResponse>, cast to actual type
const verifyResponse =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually validate the responses coming from the networkService using our validator i.e. Zod which also gives you the actual type. e.g.:

return PositionsSchema.parse(positions);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants