I am following a similar workflow as show in this StackOverflow post. I have an SPA that runs at admin.example.com
with API requests (via an OpenAPI-generated client) going to api.example.com
. The API endpoint always provides unrestricted CORS access (e.g. Access-Control-Allow-Origin: *
, etc.) as I have other CSRF-preventions in place. My objective is to prevent bad bots from accessing protected API endpoints using AWS WAF Bot Control, ideally without a user having to solve CAPTCHAs and disrupt user flow, but if the user fails Challenges, then prompt for CAPTCHA as a last attempt. My understanding is that a Challenge may fail for legitimate users, so providing a CAPTCHA as a sort of last resort would be appropriate, instead of just denying the legitimate user access to a protected resource (e.g. api.example.com/login
). I have the AWS Managed Bot Control rule with the lowest priority in my ACL, with another rule of higher priority to always allow OPTIONS pre-flight requests for the API through to the backend, so those always succeed. Both the ACL Token Domain List and the CAPTCHA Integration API key have the apex domain listed (example.com
). API calls are wrapped into the following function:
export async function applyWAFBotProtection(callback) {
window.X_AWS_WAF_TOKEN = await AwsWafIntegration.getToken();
const error = await callback();
if (error?.code === 405) {
AwsWafCaptcha.renderCaptcha(document.getElementById("waf-captcha-modal"), {
apiKey: "WAF_CAPTCHA_INTEGRATION_API_KEY",
onSuccess: (token) => {
window.X_AWS_WAF_TOKEN = token;
callback();
},
onError: (error) => {
console.error(error);
},
onPuzzleTimeout: () => {
console.error("CAPTCHA Timeout!");
},
dynamicWidth: true
});
}
}
The following middleware is applied to API calls:
if (window.X_AWS_WAF_TOKEN) {
context.setHeaderParam("x-aws-waf-token", window.X_AWS_WAF_TOKEN);
}
The following HTML is added to the <head>
tag of all HTML responses for the SPA:
<script>window.awsWafCookieDomainList = [".example.com"];</script>
<script type="text/javascript" src="{{ wafCaptchaIntegrationJSSrc }}" defer></script>
I can see that pre-flight requests make it through and CORS works for my API endpoint, then the AWS WAF token properly is attached to the headers of the API request. To force a Challenge/CAPTCHA, I've added a temporary rule above the Bot Control that always triggers a Challenge/CAPTCHA, just for testing purposes. When I set the rule to enforce a Challenge, it appears that everything works great. My understanding is that the Challenge/CAPTCHA JS SDK interrogates the client before AwsWafIntegration.getToken()
is called, so that when my AJAX/API request is made, the AWS WAF token already passes the Challenge. The problem is the CAPTCHA. I'm at the point where I think this is a AWS WAF bug, but I could be wrong. When I change the temporary rule to enforce CAPTCHA, the API request results in a 405 status code with proper CORS headers and that works fine. Then AwsWafCaptcha.renderCaptcha()
is called and I get this series of errors in the console: Network Tab Snapshot. When the SDK attempts to load image tile data from ...captcha.awswaf.com/...
for the CAPTCHA modal, the pre-flight OPTIONS request has the status code 403 and also doesn't contain Access-Control-Allow-Origin
so the following error is show in the console and the CAPTCHA runs the onError
function: Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://...captcha.awswaf.com/... (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 403.
Why doesn't AWS provide the proper CORS headers when loading its own CAPTCHA data for its own JS SDK? Applying potential solutions referenced from these articles doesn't solve this issue: AWS Blog (a general guide with working examples that are similar to my setup), AWS Docs (I'm not using CSP), AWS rePost (similar issue likely), AWS rePost (not the same 403 reason).
Running this setup in Chrome without CORS protections works perfectly and I can pass the prompted CAPTCHA, which means this is a CORS issue that seems to be out of my control (and only in AWS's control): open -n -a /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security
To summarize my main questions, along with some other side questions:
- Isn't AWS's CAPTCHA web servers supposed to provide CORS headers for its own JS SDK usage? Why is it also returning 403 for the OPTIONS pre-flight request?
- Is my assumption correct in that providing a CAPTCHA as a fallback for failing the Challenge is a good UX practice? Is the above workflow the correct approach for such functionality?
- Returning 202 for a failed Challenge and 405 for a failed CAPTCHA seems odd in the context of SPA. Interstitial status codes don't make too much of a difference, but a 202 for failed Challenge seems like a success in the context of an AJAX/API request (since it's a 2xx code). Is the StackOverflow post correct in stating that a Challenge can never fail when using the SDK? If so, what's the purpose of using it in the targeted Bot Control actions?
- Is it possible to style/theme the CAPTCHA modal? Doesn't appear to be possible. Would be nice to implement a dark theme by changing the text color and primary button color, and perhaps the font family.
- A hopeful suggestion: when a user fails the CAPTCHA, the spinning loading icon isn't big enough so it makes it look like the UI is frozen. A spinning loading icon covering the image tiles would be better.
I've been stuck on these issues for quite some time. Would be nice to get some clarity. If this post doesn't get answered, I'll reach out to AWS Support, utilizing my free trial :)