Skip to content

Class Name: ReCaptchaController

Last Updated: 2025-10-22 Source Code: https://github.com/AANP-IT/I2C.Salesforce.Metadata/blob/STAGING/force-app/main/default/classes/ReCaptchaController.cls

API Name: ReCaptchaController Type: Controller (LWC) Test Coverage: To be determined

Business Purpose

The ReCaptchaController class provides backend verification for Google reCAPTCHA v2 integration in Lightning Web Components. This controller: - Validates reCAPTCHA response tokens from the frontend - Makes callouts to Google's reCAPTCHA verification API - Protects forms from bot submissions - Returns verification status to LWC components

This supports form security for public-facing pages like registration, contact forms, and user-generated content submissions.

Class Overview

  • Author: Ryan O'Sullivan
  • Created: 2025-02-11
  • Test Class: ReCaptchaControllerTest
  • Scope/Sharing: with sharing - Respects record-level security
  • Key Responsibilities:
  • Verify reCAPTCHA tokens via Google API
  • Handle HTTP callouts securely using Named Credentials
  • Return human-readable verification status
  • Support LWC form protection

Public Methods

insertRecord

@AuraEnabled(cacheable = true)
public static String insertRecord(SObject record, String recaptchaResponse)

Purpose: Verifies a reCAPTCHA response token and returns the verification status.

Parameters: - record (SObject) - UNUSED - Placeholder for potential future record insertion - recaptchaResponse (String) - The reCAPTCHA response token from frontend

Returns: String - Verification status: - "Success - v2" - reCAPTCHA verified successfully - "Invalid Verification" - reCAPTCHA verification failed - "Invalid Verification Request" - HTTP error or empty response

Aura-Enabled Attributes: - cacheable=true: ⚠️ INCORRECT - Callout methods cannot be cacheable

Business Logic:

  1. HTTP Request Setup (lines 18-22):

    Http http = new Http();
    HttpRequest request = new HttpRequest();
    request.setEndpoint('callout:GoogleCaptcha');
    request.setMethod('POST');
    request.setBody('secret={!$Credential.Password}&response=' + recaptchaResponse);
    

  2. Uses Named Credential GoogleCaptcha for endpoint and authentication

  3. POST method per Google reCAPTCHA API requirements
  4. Body contains secret key (from credential) and user's response token
  5. Uses merge field {!$Credential.Password} for secret injection

  6. HTTP Callout (line 23):

    HttpResponse response = http.send(request);
    

  7. Synchronous callout to Google's verification API

  8. Returns JSON response with verification result

  9. Response Parsing (lines 25-38):

    if (response.getStatusCode() == 200) {
        String responseBody = response.getBody();
        if (String.isNotBlank(responseBody)) {
            Map<String, Object> result = (Map<String, Object>) JSON.deserializeUntyped(responseBody);
            if (result.containsKey('success') && (Boolean) result.get('success')) {
                return 'Success - v2';
            } else {
                return 'Invalid Verification';
            }
        } else {
            return 'Invalid Verification Request';
        }
    }
    return 'Invalid Verification Request';
    

Response Handling: - 200 Status: Successful HTTP call - Non-blank Body: Valid JSON response - success = true: reCAPTCHA verified (returns "Success - v2") - success = false: Bot detected or token invalid - Blank Body or Non-200: HTTP error

  1. Exception Handling (lines 39-41):

    catch (Exception e) {
        throw new AuraHandledException('An error occurred while fetching reCAPTCHA: ' + e.getMessage());
    }
    

  2. Catches all exceptions (CalloutException, JSONException, etc.)

  3. Wraps in AuraHandledException for LWC error handling
  4. Exposes exception message to frontend

Issues/Concerns: - 🚨 CRITICAL: cacheable=true (line 15): Callout methods CANNOT be cacheable - Salesforce will throw exception at runtime - Remove cacheable attribute - ⚠️ Unused Parameter: record parameter not used (line 16) - Misleading method name "insertRecord" - Should rename to verifyRecaptcha - ⚠️ Exception Message Exposure: Line 40 exposes technical details to frontend - Could reveal API configuration details - Consider generic error message - ⚠️ No Response Code Validation: Only checks for 200, doesn't handle 4xx/5xx - ⚠️ No Timeout Configuration: HTTP request uses default timeout - ⚠️ Hardcoded Template Name: "AANP_FORGOT_PASSWORD" appears unrelated - ✅ Named Credential: Uses secure Named Credential pattern - ✅ Merge Field: Secret key not hardcoded (uses {!$Credential.Password})

Google reCAPTCHA API Response Format:

{
  "success": true|false,
  "challenge_ts": "timestamp",
  "hostname": "example.com",
  "error-codes": ["missing-input-secret", "invalid-input-secret", ...]
}

LWC Usage Example:

import { LightningElement } from 'lwc';
import insertRecord from '@salesforce/apex/ReCaptchaController.insertRecord';

export default class ContactForm extends LightningElement {
    async handleSubmit() {
        const recaptchaToken = await this.getRecaptchaToken();

        try {
            const result = await insertRecord({
                record: null, // Not used
                recaptchaResponse: recaptchaToken
            });

            if (result === 'Success - v2') {
                // Proceed with form submission
                this.submitForm();
            } else {
                // Show error message
                this.showError('Please complete the reCAPTCHA');
            }
        } catch (error) {
            this.showError(error.body.message);
        }
    }

    async getRecaptchaToken() {
        // Execute reCAPTCHA v2 challenge
        return await grecaptcha.execute();
    }
}

Recommended Improvements:

// Remove cacheable, rename method, improve error handling
@AuraEnabled
public static String verifyRecaptcha(String recaptchaResponse) {
    if (String.isBlank(recaptchaResponse)) {
        throw new AuraHandledException('reCAPTCHA response is required.');
    }

    try {
        HttpRequest request = new HttpRequest();
        request.setEndpoint('callout:GoogleCaptcha');
        request.setMethod('POST');
        request.setTimeout(10000); // 10 second timeout
        request.setBody('secret={!$Credential.Password}&response=' + recaptchaResponse);

        Http http = new Http();
        HttpResponse response = http.send(request);

        if (response.getStatusCode() != 200) {
            System.debug(LoggingLevel.ERROR, 'reCAPTCHA HTTP error: ' + response.getStatusCode());
            throw new AuraHandledException('Unable to verify reCAPTCHA. Please try again.');
        }

        String responseBody = response.getBody();
        if (String.isBlank(responseBody)) {
            throw new AuraHandledException('Invalid reCAPTCHA response.');
        }

        Map<String, Object> result = (Map<String, Object>) JSON.deserializeUntyped(responseBody);

        if (result.containsKey('success') && (Boolean) result.get('success')) {
            return 'Success';
        } else {
            // Log error codes for debugging
            if (result.containsKey('error-codes')) {
                System.debug('reCAPTCHA errors: ' + result.get('error-codes'));
            }
            return 'Invalid Verification';
        }

    } catch (CalloutException e) {
        System.debug(LoggingLevel.ERROR, 'reCAPTCHA callout failed: ' + e.getMessage());
        throw new AuraHandledException('Network error. Please try again.');
    } catch (Exception e) {
        System.debug(LoggingLevel.ERROR, 'reCAPTCHA verification error: ' + e.getMessage());
        throw new AuraHandledException('Verification failed. Please try again.');
    }
}

Dependencies

Salesforce Objects

  • None

Custom Settings/Metadata

  • Named Credential: GoogleCaptcha
  • Endpoint: https://www.google.com/recaptcha/api/siteverify
  • Authentication: Contains secret key in Password field
  • Required for callout

Other Classes

  • None (standalone controller)

External Services

  • Google reCAPTCHA API:
  • Endpoint: https://www.google.com/recaptcha/api/siteverify
  • Method: POST
  • Authentication: Secret key in request body
  • Response: JSON with success boolean

Design Patterns

  1. Named Credential Pattern: Secure API authentication without hardcoding
  2. LWC Controller Pattern: @AuraEnabled for Lightning Web Component integration
  3. Exception Wrapping: Converts system exceptions to AuraHandledException
  4. HTTP Callout Pattern: Standard Salesforce HTTP API usage

Governor Limits Considerations

Current Impact (Per Transaction)

  • HTTP Callouts: 1 callout per verification
  • Callout Timeout: Default (10 seconds unless configured)
  • Heap Size: Minimal (small JSON response)
  • CPU Time: Minimal (JSON parsing only)

Limits

  • Maximum Callouts: 100 per transaction
  • Maximum Callout Time: 120 seconds total
  • Named Credentials: No limit on usage

Scalability Analysis

  • Single Verification: One callout per method invocation
  • ⚠️ No Bulk Support: Cannot verify multiple tokens in single call
  • ⚠️ Synchronous: Blocks until Google responds
  • Low Resource Usage: Minimal heap and CPU

Recommendations

  1. Timeout Configuration: Set explicit timeout (10 seconds) to fail fast
  2. Retry Logic: Consider retry on network errors (in LWC, not Apex)
  3. Caching: Do NOT cache results (each token is single-use)

Error Handling

Exception Types Thrown

  • AuraHandledException: All error scenarios wrapped in this type

Exception Types Caught

  • Exception (line 39): Generic catch-all including:
  • CalloutException: Network errors, timeouts
  • JSONException: Malformed response
  • TypeException: Type casting errors

Error Handling Strategy

  • Try-Catch Wrapper: Entire method wrapped in try-catch
  • Apex to LWC: AuraHandledException for LWC error handling
  • User-Friendly Messages: Generic error messages for users
  • Debug Logging: Technical details logged via System.debug

Error Handling Gaps

  1. Exception Message Exposure: Line 40 exposes technical details
  2. No HTTP Status Differentiation: Doesn't distinguish 4xx vs 5xx errors
  3. No Retry Logic: Single attempt only
  4. No Error Code Parsing: Doesn't parse Google's error-codes array

Monitoring Recommendations

// Query callout logs
SELECT Id, Request, Response, HttpStatusCode, CreatedDate
FROM HttpCalloutLog
WHERE CreatedDate = TODAY
  AND URL LIKE '%google.com/recaptcha%'
ORDER BY CreatedDate DESC

// Monitor via Platform Event
EventBus.publish(new ReCaptcha_Verification__e(
    Status__c = 'Failed',
    Error_Message__c = e.getMessage(),
    Response_Token__c = recaptchaResponse.left(10) + '...'
));

Security Considerations

API Security

  • Named Credential: Secret key not exposed in code
  • Merge Field: Uses {!$Credential.Password} for secret injection
  • ⚠️ No Token Validation: Doesn't validate token format before callout
  • ⚠️ Exception Exposure: Technical errors exposed to frontend

reCAPTCHA Security

  • Token Single-Use: Each token valid for one verification only
  • Time-Limited: Tokens expire after ~2 minutes
  • Domain Validation: Google validates request origin domain
  • Bot Protection: Analyzes user behavior to detect bots

Best Practices

  1. Frontend Integration: Load reCAPTCHA from Google CDN
  2. Site Key: Configure site key in LWC (public key)
  3. Secret Key: Store in Named Credential (never expose to frontend)
  4. Score Threshold: For reCAPTCHA v3, define acceptable score (0.0-1.0)

Test Class Requirements

Required Test Coverage

See ReCaptchaControllerTest (test class exists)

Mock Callout Requirements

@IsTest
global class GoogleRecaptchaMock implements HttpCalloutMock {
    global HTTPResponse respond(HTTPRequest req) {
        HttpResponse res = new HttpResponse();
        res.setHeader('Content-Type', 'application/json');
        res.setStatusCode(200);

        // Mock successful verification
        res.setBody('{"success": true, "challenge_ts": "2025-02-11T12:00:00Z", "hostname": "example.com"}');

        return res;
    }
}

@IsTest
public class ReCaptchaControllerTest {
    @IsTest
    static void testInsertRecord_Success() {
        Test.setMock(HttpCalloutMock.class, new GoogleRecaptchaMock());

        Test.startTest();
        String result = ReCaptchaController.insertRecord(null, 'test-token-12345');
        Test.stopTest();

        Assert.areEqual('Success - v2', result, 'Should return success');
    }

    @IsTest
    static void testInsertRecord_Failed() {
        Test.setMock(HttpCalloutMock.class, new GoogleRecaptchaFailMock());

        Test.startTest();
        String result = ReCaptchaController.insertRecord(null, 'invalid-token');
        Test.stopTest();

        Assert.areEqual('Invalid Verification', result, 'Should return invalid');
    }
}

Changes & History

Date Author Description
2025-02-11 Ryan O'Sullivan Initial implementation for reCAPTCHA v2 verification
(Current) - Documentation added

Pre-Go-Live Concerns

🚨 CRITICAL

  • cacheable=true on Callout Method (line 15): Will cause runtime exception
  • Remove cacheable=true attribute immediately
  • Callout methods cannot be cached

HIGH

  • Exception Message Exposure (line 40): Technical details exposed to users
  • Use generic error messages for frontend
  • Log technical details via System.debug only

MEDIUM

  • Misleading Method Name: "insertRecord" doesn't insert anything
  • Rename to verifyRecaptcha for clarity
  • Remove unused record parameter
  • No Timeout Configuration: Uses default HTTP timeout
  • Set explicit 10-second timeout
  • No Error Code Handling: Doesn't parse Google's error-codes array
  • Log error codes for debugging

LOW

  • No Response Validation: Doesn't check for expected JSON structure
  • Hardcoded Success String: "Success - v2" could be constant

Maintenance Notes

📋 Monitoring Recommendations

  • Verification Success Rate: Track percentage of successful verifications
  • Callout Failures: Monitor HTTP errors and timeouts
  • Bot Detection Rate: Track "Invalid Verification" responses
  • API Quota: Monitor Google reCAPTCHA API usage limits

🔧 Future Enhancement Opportunities

  1. reCAPTCHA v3: Migrate to score-based v3 for better UX
  2. Score Threshold: Implement configurable score threshold for v3
  3. Retry Logic: Add retry for transient network errors
  4. Response Caching: Cache token validation to prevent replay attacks (with short TTL)

⚠️ Breaking Change Risks

  • Removing record parameter requires LWC updates
  • Changing return values breaks existing LWC error handling
  • Renaming method requires refactoring all callsites
  • reCAPTCHA LWC: Frontend component that generates tokens
  • GoogleCaptcha Named Credential: API authentication configuration
  • Public Forms: Registration, contact, content submission forms

Business Owner

Primary Contact: Security / IT Team Technical Owner: Ryan O'Sullivan / Salesforce Development Team Created: 2025-02-11 Last Reviewed: [Date]


Documentation Status: ✅ Complete Code Review Status: 🚨 CRITICAL - Remove cacheable=true before deployment Test Coverage: Test class exists (ReCaptchaControllerTest) External Dependency: Google reCAPTCHA API