Putting directives, services, and observables to use. Invisible + visible reCAPTCHA.
I’m not a robot. I am told, however, that they occasionally try to sign up for life insurance.
HavenLife does not offer artificial life policies (yet), so for our recent brand relaunch we added several reCAPTCHAs to protect against spam and other automated abuse on email registration forms.
Google offers three types of reCAPTCHA:
- Visible — requires the user to click a checkbox verifying
I'm not a robot
- Invisible — does not require a checkbox click, instead it is invoked directly when the user clicks on an existing button
- Android — native Android reCAPTCHA from Google Play services
Design specs called for two visible reCAPTCHAs and one invisible reCAPTCHA on the new site. Adding multiple reCAPTCHAs to a single-page app adds a degree of complexity to the implementation. Integrating both visible and invisible reCAPTCHAs on an Angular 2 application proved even more challenging. In this article, we’ll go through the steps taken to add reCAPTCHA to our app and highlight a few lessons learned.
reCAPTCHA + Angular2
An uneasy pairing
Google maintains both the reCAPTCHA and Angular libraries and yet, like many siblings, the two don’t really get along. Angular 2 manages DOM manipulation using a modular system of components and templates. Functions on the
Window object explicitly render reCAPTCHA. Uh oh, Drama!
Disclaimer: it is best practice in Angular to not reference the DOM directly, including the
Windowobject. In the HavenLife application we use a simple service for injecting the
Windowobject into our component. For clarity in this article, we removed mention of this service and reference the window directly. Please note this is neither ideal nor advisable. Consider yourself warned. 👀
Directive and initialization
recaptcha.directive.ts begins the implementation by listening for a creatively-named
recaptcha selector found in HTML form templates. This directive houses
Window functions to append the reCAPTCHA widget to the DOM and manages the client-side calls to Google.
In the directive, we start by declaring
widgetId variables and importing relevant Angular libraries.
key stores the site-key provided by Google upon registration and can be accessed via configs.
Pro Tips: Google test keys are available that do not require registration and will always pass — never requiring users to complete a test question. These are great for automated testing but won’t catch bugs so use with caution. For local testing, you will need to register and use your local IP address, usually 127.0.0.1, as localhost is explicitly blocked.
While other writing on the integration of Angular2 and reCAPTCHA suggest creating a global variable to hold
grerecaptcha, we chose to simply declare a component-level
var of type
<any> . We avoided creating a global to limit exposure in the larger codebase. As always, do what works best in your context. 🤓
Next, to render the reCAPTCHA,
ngOnInit() gets the site-key and then calls two functions
onload parameter to the name of our callback function (
render parameter to
addScript also uses vanilla JS to remove any old copies of the reCAPTCHA script tag before appending to the parent. This prevents the error
ReCAPTCHA placeholder element must be empty that can occur if the user encounters multiple reCAPTCHAs in a single session.
We are now ready to render our widget!
renderRecaptchaWithCallback() sets up the config params (outlined in the docs) with styling parameters like
tabindex as well as callbacks for success and reCAPTCHA expiration.
Note: this example outlines steps to implement a visible reCAPTCHA, the invisible reCAPTCHA programmatically calls
grecaptcha.execute()to invoke the challenge. More info can be found in the docs.
The next step is to define the
onSuccess callback. This function will get called when all the dependencies have loaded and returns a token used to validate on the server-side. We use an Angular service to manage tokens generated by the
onSuccess callback and made the token available as an Observable Subject .
onExpired() callback handles cases where the user completes the reCAPTCHA successfully but does not finish the email registration.
recaptcha.service.ts clears the token and calls upon another component-level
var instance of
grecaptcha to reset the reCAPTCHA.
retry function can also be used to handle any errors returned by the server-side validation. You will want to develop your own protocol for handling reCAPTCHA attempts and error messaging, but a service can be useful for managing this communication across the application.
Once we pass the generated token to the server-side, we call
verifyRecaptcha() to verify that the user token is on the up-and-up before going any further in the email registration process. An HTTP request client with Promise support makes for easy error handling.
The Google reCAPTCHA API returns the following response JSON object and error codes. They are not terribly forthcoming, however, about the cause of the errors for
invalid-input-response which would ostensibly encompass any malicious token use.
Invisible + Visible ReCAPTCHAs Together
Worth the Hassle?
Initial designs from our UX team called for 3 instances — two visible and one invisible — of reCAPTCHA. The initial purpose of
addScript was to also toggle the configs and site-keys for the visible and invisible reCAPTCHAs, each of which require a separate registration with Google. Due to the slightly different client-side functions necessary to execute and render the reCAPTCHA’s we then split these out into two separate sets of directives and services. Nonetheless, on pages with two different types of reCAPTCHA we were sometimes seeing errors from the two instances of
grecaptcha or the Google
script tag. We addressed each edge case; the code grew bloated.
Ultimately, it was a design and business review that determined two types of reCAPTCHA are not worth it.
Spoiler alert: the invisible reCAPTCHA isn’t entirely invisible. It requires the presence of a badge from which the followup challenge can be launched. It is possible to obscure the badge with CSS, but it is still the site of a popup in the case of a problematic user. Users found the sudden appearance of the reCAPTCHA challenge disorienting and it discouraged email registration.
On their own, a reCAPTCHA might not be sufficient to take down an army of determined Cylons. Rather reCAPTCHAs compose one small portion of a broad, ever-evolving defense-in-depth strategy. Perhaps a Googler will one day unite the disagreeable progeny reCAPTCHA and Angular2 into a single module. Until then, hopefully we helped.