Networking & Content Delivery
Optimizing web application user experiences with AWS WAF JavaScript integrations
AWS WAF Bot Control uses CAPTCHA and Challenge actions to undertake a browser interaction before permitting requests to protected resources. These actions can result in a poor user experience because of application errors or unexpected CAPTCHA completion when AWS WAF unexpectedly blocks requests. The AWS WAF JavaScript integrations give you the ability to control these browser interactions in a method that both maximizes protection against bots while also providing the best user experience.
In this post, we provide scenarios for how you can optimize user experiences when integrating your browser-based Single Page Applications (SPAs) or Server Side Rendered (SSR) applications with CAPTCHA and Challenge actions using the AWS WAF JavaScript integrations.
Considerations before using the AWS WAF JavaScript integrations
There are two considerations to make before you start using the AWS WAF JavaScript integrations:
- Do you know in advance when CAPTCHA or Challenge actions are triggered?
- Does the browser communicate with AWS WAF protected resources across domains?
Do you know in advance when CAPTCHA or Challenge actions are triggered?
AWS WAF rules can be matched in two ways: deterministically based on individual request characteristics, or non-deterministically based on the characteristics of a series of requests. For example, a rule matching the /checkout
URL with a CAPTCHA action is deterministic because you know in advance that users need to complete a CAPTCHA prior to making the request. Alternatively, rules such as the AWS WAF Targeted Bot Control TGT_VolumetricSession
need CAPTCHA completion when a series of requests over five minutes exceed a dynamic threshold. This is non-deterministic because you can’t advance when a CAPTCHA is needed.
Knowing when your rule action triggers drives how you integrate CAPTCHA and Challenge into your application. Deterministic rule evaluation means that you can handle these actions at a time that best suits the user journey. Non-deterministic rule evaluation means your application needs to handle these actions at any point that it makes a request to protected resources. Not handling these actions can mean application errors and a broken user experience. This can be a particularly frustrating user experience when it happens randomly because of non-deterministic rule evaluation.
Does the browser communicate with AWS WAF protected resources across domains?
A browser sends requests to protected resources across one or more domains and AWS WAF web access control lists (web ACLs). Figure 1 illustrates three protected resource communication scenarios that you may encounter:
- Single Domain: The browser requests protected resources directly from a single domain, such as HTML content from www.example.com.
- Shared Apex Domain: The browser requests protected resources across a shared apex domain, for example, www.example.com and api.example.com both share the apex domain example.com.
- Distinct Domains: The browser requests protected resources across multiple domains but there is no shared apex (public suffixes such as .com are excluded). For example, www.other.com and www.example.com have no shared apex.
Requests across domains and web ACLs for sub-resources (for example images), or JavaScript fetch
calls may be blocked by AWS WAF rules with CAPTCHA and Challenge actions leading to a broken user experience.
Integration options for web applications
CAPTCHA and Challenge actions can be completed by a web browser in two ways: interstitial (during page load), or the AWS WAF JavaScript integrations.
Interstitial
Rules matching a CAPTCHA or Challenge action return a script interstitially (during page load) for HTML GET requests (identified by the header Accept: text/html
). Figure 2 shows how selecting the Challenge link for a page protected by Challenge has a short delay while the challenge is completed prior to the original requested page being loaded. Similarly, Figure 3 shows how the CAPTCHA is displayed prior to the original requested page being loaded.
Interstitial completion is an effective method for using CAPTCHA and Challenge actions. However there are user experience impacts that you should be aware of:
- Delayed page load: Interstitial completion downloads and runs an AWS WAF script prior to loading the requested resource. This can cause a perceptible pause in your application loading.
- Disrupted page loads: CAPTCHA needs more time for the user to complete the problem. This interrupts their user journey and potentially causes user drop off. CAPTCHA or Challenge actions triggered non-deterministically also cause seemingly random pauses or interruptions to a user journey.
- Requests for non-HTML content can fail: JavaScript
fetch
calls, static resources such as images, and non-GET requests receive a non-200 HTTP response when either action is triggered. This may appear as a broken image, failed form submission, or error in a JavaScript frontend.
These issues can be mitigated by using the AWS WAF JavaScript integrations, which is the recommended approach for optimizing user experience.
AWS WAF JavaScript integrations
AWS WAF has two separate JavaScript integrations for controlling CAPTCHA and Challenge interactions. These integrations are available when you add either AWS WAF Targeted Bot Control or AWS WAF Fraud Control managed rule sets to a web ACL.
- Intelligent threat API: This completes a proof-of-work challenge, and acquires a token that AWS WAF uses to validate requests that match a rule with a Challenge action. You integrate with the Intelligent threat API by including the script URL in the
<head>
of your application’s HTML pages:<head> <script type="text/javascript" src="integration URL/challenge.js" defer></script> <head>
This script completes this during page load and refreshes the token automatically. Further guidance on retrieving the integration URL can be found in our documentation.
- CAPTCHA JavaScript API: You display a CAPTCHA programmatically using the
renderCaptcha
JavaScript function prior to sending requests that match a rule with a CAPTCHA action. You integrate with the CAPTCHA integration API by including the script URL in the<head>
of your application’s HTML pages:<head> <script type="text/javascript" src="integration URL/jsapi.js" defer></script> </head> <script type="text/javascript"> function showMyCaptcha() { var container = document.querySelector("#my-captcha-container"); AwsWafCaptcha.renderCaptcha(container, { apiKey: "...API key goes here...", onSuccess: captchaExampleSuccessFunction, onError: captchaExampleErrorFunction, ...other configuration parameters as needed... }); } ... function captchaExampleSuccessFunction(wafToken) { // Use WAF token to access protected resources AwsWafIntegration.fetch("...WAF-protected URL...", {
Guidance on retrieving the integration URL and API key can be found in our documentation. The CAPTCHA JavaScript API includes the Intelligent threat API so you don’t need to include it separately. Rules with a Challenge action are also permitted.
The integrations also collect client side telemetry data that is used by Targeted Bot Control rules to separate legitimate traffic from malicious bot traffic. We recommend that you integrate with these SDKs to optimize user experience and maximize the effectiveness of bot control rules.
Scenario 1: Requesting protected resources across domains
JavaScript integrations default to generating a token that is only accepted by AWS WAF for the web application’s host domain (for example www.example.com). Users face more CAPTCHAs/Challenges when going to different domains protected by AWS WAF. fetch
and non-GET requests (for example form submission) receive HTTP status codes 202 (Challenge) or 405 (CAPTCHA), which leads to application errors and a broken user experience. Requests may also succeed during testing but be blocked randomly in production because of non-deterministic rules triggering these actions unexpectedly. Minimize disruption and negative user experience for both cross domain scenarios:
- Shared apex domain: Configure the apex domain in the
<head>
of your application’s HTML so that the token is accepted for the sub domains, for example under example.com:<head> window.awsWafCookieDomainList = ['.example.com'] <script type="text/javascript" src="integration URL/challenge.js" defer></script> <head>
- Distinct Domains: Browser security settings mean users redirected to different domains (for example from www.example.com to www.other.com) need to complete a separate CAPTCHA/Challenge. Furthermore, POST form submissions are not possible in this scenario because they are missing a valid token.
fetch
requests to different domains (for example www.example.com to www.other.com) can include the token by either using theAwsWafIntegration.fetch
wrapper, or manually adding the token as the headerx-aws-waf-token
to your existingfetch
request:AwsWafIntegration.getToken().then(token => fetch('/url', { headers: { 'x-aws-waf-token': token, }, });
Tokens are valid across web ACLs, but you do need to set the token domain setting in each web ACL to include the requesting host domain (for example www.example.com) or shared apex domain (example.com) for them to be accepted. We have another post, Using AWS WAF intelligent threat mitigations with cross-origin API access, detailing cross domain scenarios in more depth.
Scenarios for optimizing user experience in SSR applications
The architecture in Figure 4 shows an SSR application served by an AWS WAF protected Amazon CloudFront distribution. We now consider scenarios for how you can optimize the user experience for this architecture.
Scenario 2: Handling deterministic and non-deterministic CAPTCHA actions
The AWS WAF JavaScript integrations automatically re-challenge in the background. This means subsequent requests don’t encounter a deterministic or non-deterministic interstitial challenge completion. You can avoid interstitial CAPTCHA completions by rendering the CAPTCHA puzzle using JavaScript in your application at a time that makes sense in the user journey, and prior to deterministically evaluated rules with CAPTCHA actions.
Interstitial CAPTCHA completions can still occur for non-deterministically evaluated rules because you don’t know when to display the CAPTCHA prior to redirecting the user. There are two options to minimize user journey drop off because of inopportune interstitial CAPTCHAs:
- Presenting a CAPTCHA early in the user journey: Have the user complete a CAPTCHA at log in, prior to requests that may trigger a CAPTCHA. Avoid subsequent disruptions by setting the immunity time to the length of the user’s session.
- Using Challenge instead of CAPTCHA: Targeted Bot Control has some rules with a CAPTCHA action default. Trade off user experience with bot control efficiency by switching the action from CAPTCHA to Challenge.
Scenario 3: Avoiding blocked static resource requests from breaking the user experience
Browser requests for static resources such as images without a valid token fail if they match rules with a CAPTCHA/Challenge action. This can occur even when using the JavaScript integrations because the <script>
’s defer
attribute means token acquisition happens at the end of page load. Avoid broken images and other negative user experiences by excluding these resources from rules with CAPTCHA/Challenge actions. Refer to the guide in our documentation for how to scope down the Targeted Bot Control ruleset to only evaluate requests for dynamic content including HTML pages.
Scenarios for optimizing user experience in SPAs
The architecture in Figure 5 shows an AWS WAF protected CloudFront distribution with fetch requests to an application at api.example.com from an SPA served from an Amazon S3 bucket at www.example.com. Now we consider scenarios for how you can optimize the user experience for this architecture.
Scenario 4: Optimizing page rendering while completing challenge
Improve page load performance by switching the defer
attribute in the Intelligent threat SDK <script>
tag to async
so that the script is loaded in parallel with the rest of your SPA:
<head>
window.awsWafCookieDomainList = ['.example.com']
<script type="text/javascript" src="integration URL/challenge.js" async></script>
<head>
The challenge also may have not completed by the time your SPA makes its first fetch
call to a protected resource. You have two options to avoid sending requests prior to the challenge being completed:
- Use
AwsWafIntegration.fetch
wrapper: makes sure the challenge has completed prior to making thefetch
request. - Use
AwsWafIntegration.getToken()
: returns when the challenge has completed, after which you can use your originalfetch
request to the protected resource:AwsWafIntegration.getToken() .then(token => { //make your original fetch })
Scenario 5: Handling deterministic and non-deterministic CAPTCHA actions
As with SSR applications, resources protected by a deterministic rule with a CAPTCHA action mean that you can render the puzzle using JavaScript at a time that best fits in the user journey. For SPAs you can use a modal to avoid disrupting the existing DOM structure. Figure 6 demonstrates an example React application displaying a CAPTCHA modal after choosing the Login button. Example code can be found on GitHub.
This uses a modified version of fetch
that intercepts the call and completes the CAPTCHA process prior to submitting the original call to the protected resource:
function captchaFetch (input, init) {
document.body.style.cursor = 'wait'
document.getElementById('modalOverlay').style.display = 'block'
document.getElementById('modal').style.display = 'block'
return new Promise((resolve) => {
window.AwsWafCaptcha.renderCaptcha(document.getElementById('captchaForm'), {
onSuccess: () => {
document.getElementById('modalOverlay').style.display = 'none'
document.getElementById('modal').style.display = 'none'
resolve(window.AwsWafIntegration.fetch(input, init))
},
onLoad: () => {
document.body.style.cursor = 'default'
},
apiKey: getWAFEnv().CAPTCHA_API_KEY
})
})
}
// replace original fetch with captchaFetch:
captchaFetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
credentials: 'include'
})
Non-deterministic rules with CAPTCHA actions mean you must display a CAPTCHA at any point you make a request to a protected resource. Figure 7 demonstrates a CAPTCHA modal being displayed after adding an arbitrary number of items to a to-do list.
Non-deterministic CAPTCHA actions return a 405 HTTP status code that your application needs to handle. Our documentation has an example that recursively completes a fetch
if the request receives a 405 response.
If your SPA can’t handle non-deterministic CAPTCHAs, then you can change the rule actions to Challenge. This trades off user experience with bot control efficiency because it is less likely that a human is making the request.
Scenario 6: Avoiding blocked static resource requests from breaking the user experience
Refer to this scenario under the SSR application section because it also applies here. We recommend that you only evaluate rules with CAPTCHA and Challenge actions against dynamic content (that is API calls through fetch
).
Conclusion
In this post we provided guidance on how you can optimize the user experience of your SPA and SSR web-applications when using the AWS WAF JavaScript integrations. This enables you to maximize the effectiveness of your bot control while also minimizing the likelihood of user journey drop off because of negative experiences.