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:
-
HTTP Request Setup (lines 18-22):
-
Uses Named Credential
GoogleCaptchafor endpoint and authentication - POST method per Google reCAPTCHA API requirements
- Body contains secret key (from credential) and user's response token
-
Uses merge field
{!$Credential.Password}for secret injection -
HTTP Callout (line 23):
-
Synchronous callout to Google's verification API
-
Returns JSON response with verification result
-
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
-
Exception Handling (lines 39-41):
-
Catches all exceptions (CalloutException, JSONException, etc.)
- Wraps in AuraHandledException for LWC error handling
- 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¶
- Named Credential Pattern: Secure API authentication without hardcoding
- LWC Controller Pattern: @AuraEnabled for Lightning Web Component integration
- Exception Wrapping: Converts system exceptions to AuraHandledException
- 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¶
- Timeout Configuration: Set explicit timeout (10 seconds) to fail fast
- Retry Logic: Consider retry on network errors (in LWC, not Apex)
- 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¶
- Exception Message Exposure: Line 40 exposes technical details
- No HTTP Status Differentiation: Doesn't distinguish 4xx vs 5xx errors
- No Retry Logic: Single attempt only
- 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¶
- Frontend Integration: Load reCAPTCHA from Google CDN
- Site Key: Configure site key in LWC (public key)
- Secret Key: Store in Named Credential (never expose to frontend)
- 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=trueattribute 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
verifyRecaptchafor clarity - Remove unused
recordparameter - 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¶
- reCAPTCHA v3: Migrate to score-based v3 for better UX
- Score Threshold: Implement configurable score threshold for v3
- Retry Logic: Add retry for transient network errors
- Response Caching: Cache token validation to prevent replay attacks (with short TTL)
⚠️ Breaking Change Risks¶
- Removing
recordparameter requires LWC updates - Changing return values breaks existing LWC error handling
- Renaming method requires refactoring all callsites
🔗 Related Components¶
- 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