When deploying a CAP application on BTP that is publicly accessible and involves user input, safeguarding it from automated bots is crucial. This can be effectively achieved through the implementation of reCAPTCHA, a Google service designed to distinguish between human users and bots. As illustrated by my own public website, https://wouter.lemaire.tech , developed using CAP and hosted on BTP, the integration of reCAPTCHA adds a vital layer of security to ensure a seamless user experience.
In this blog post, I’ll demonstrate how I implemented this for my own website.
Before we start implementing reCAPTCHA, we first have to create a reCAPTCHA site. This can be done on the reCAPTCHA website: https://www.google.com/recaptcha/about/
(You’ll need to login with a google account first)
Go to “v3 Admin Console”:
Click on the “+” button to create a new site:
Here you can select a reCAPTCHA type, this will have impact on how it will be shown on your website. Do you want the end-user to click on a tickbox first? Do you prefer to have a smarter version based on score? Depending on your needs, you might want to take a different type:
Once created, it will provide you a site key and secret key. Store them somewhere, you’ll need it during the implementation!
Provide the domains for the website that can trigger the reCAPTCHA. I’ve put it also the BTP domains to be able to test from in VSCode and BAS but also to test if it works before I had a custom domain.
You can also configure the security settings depending on your needs:
Once you have done this, we can start using reCAPTCHA in our CAP application.
Before starting on the implementation, an overview of what we will be implementing. reCAPTCHA requires implementation on the frontend (UI5 in our case) and on the backend (service layer in CAP). It is up to us to implement it correct to secure the application with reCAPTCA.
Steps to be implemented to make sure that it follows the correct flow:
When the result of step 6 is successful you can continue with your own implementation assuming that the input is coming from a real user and not a bot.
Let’s have some fun! We’re going to start from top to bottom which brings us first to the UI. In my example, I use the reCAPTCHA tickbox in a dialog that has his own controller. For this I added a FlexBox in the dialog as placeholder for the reCAPTCHA tickbox.
Here I added one line with the id “captcha” to use it later on to connect it with reCAPTCHA:
Next, we need to implement the rendering of the reCAPTCHA tickbox from in the controller:
My dialog controller has a custom function “onBeforeShow” that will be called every time it is being opened. In this function I added the following code to render the reCAPTCHA tickbox. The tickbox requires the dialog to be rendered before it can be shown. Therefore, I use the event “afterOpen” to render the tickbox. The reCAPTCHA tickbox requires the dom id of the flexbox (not just the UI5 id), sitekey and a callback. The callback is used when a user selects the tickbox. When a user clicks on the tickbox, it will send a request automatically to the google reCAPTCHA api to validate if it is a real user and response with a captcha token which will be received in the callback.
(this.dialog.fragment as Dialog).attachAfterOpen((event: UI5Event) => {
const id = (this.fragmentById(this.viewController, "Book", "captcha") as FlexBox)?.getDomRef()?.id;
grecaptcha.ready(() => {
grecaptcha.render(id, {
'sitekey': '<sitekey>',
'callback': (response: string) => this.verifyCallback(response)
});
});
});
In the callback, the response contains the captcha token and I store it in a JSON Model.
public verifyCallback(response: string) {
const bookModel = this.dialog.fragment.getModel("book")! as JSONModel;
bookModel.setProperty("/captcha", response);
}
Once the user wants to save or submit his input, the token needs to be used in the request to the CAP backend. In my example, I’m using an action with a property for captcha but this could also be an entity or it could also be passed as a header parameter:
const bookModel = this.dialog.fragment.getModel("book")! as JSONModel;
const mailCtx = (this.viewController.getView()?.getModel() as ODataModel)?.bindContext("/sendMail(...)");
mailCtx.setParameter("mailFrom", bookModel.getProperty("/mail"));
mailCtx.setParameter("topic", bookModel.getProperty("/topic"));
mailCtx.setParameter("description", bookModel.getProperty("/description") || "");
mailCtx.setParameter("captcha", bookModel.getProperty("/captcha") || "");
await mailCtx.execute();
That’s it for the UI! But before starting on the backend implementation, we need to add some config in the package.json:
We’ll have to send a request from the backend to the google reCAPTCHA api. It is recommended to declare external services/api’s in the package.json. A destination could also be used but in this case doesn’t has much added value:
In the srv layer of our CAP backend we’ll need to create the action:
In the cat-service.cds, I only declared an action:
In the event handler of this action, I use the external service configuration from the package.json. this connection can be used to send a request that verifies the captcha token together with the secret key. (I’m sending it as part of the body and url parameters, normally url parameters is enough)
The result of this request will verify if it was successful and the end-user is validated as a real user:
const { data: { mailFrom, topic, description, captcha }}=req;
const captchaAPI = await cds.connect.to("RECAPTCHA_API");
const data = {
secret: "<captcha secret key>",
response: captcha
};
const validateCaptcha = await captchaAPI.send({ method: 'POST', path: `/siteverify?secret=${data.secret}&response=${data.response}`, data });
console.log(JSON.stringify(validateCaptcha))
if (!validateCaptcha.success) {
console.error("404 Captcha not correct!");
req.error(404, `Your session is not valid anymore, try to refresh your browser.`);
}
That's it! 🙂
Feel free to try it on my website and put in the description that it is just for testing purpose 😉
Go to https://wouter.lemaire.tech/ and click on “Book me”:
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
13 | |
5 | |
4 | |
4 | |
3 | |
3 | |
3 | |
3 | |
3 | |
3 |