feat: secure API using Cloudflare's Turnstile
Fighting h@ckers with Cloudflare's Turnstile 😉
See https://github.com/bdebon/choiceof.dev/issues/100 for details and discussion.
@bdebon: I pushed this yesterday evening but didn't take time to explain the feature and how to set it up 🥱
That's all very simple but let me do it before considering merging 😉
I read the pr, superb refactorings here and there and implementation of this turnstile thing that I have to confess I don't understand 100% for now (I have never used cloudflare so I'm missing some things here). Waiting for your explanation when you have time but super hyped about the content of this PR! Thank you so much!
Here's my proper MR message ⚡
Why?
We need a way to prevent spam 💀
How?
Use Cloudflare's Turnstile feature (pretty much like reCAPTCHA).
What steps do we need?
On page load:
- Load Turnstile JS API
- Initialize an invisible widget
- Request a token
When submitting a choice:
- [Front] Send the token with the chosen position
- [Back] Check if it exists and if Cloudflare validates it (it can only be used once)
- [Front] Request a new token for the next choice
What's required?
We need to set up a Turnstile site (any Cloudflare account can do this). This will give us a site and a secret keys.
I created a "localhost" pair and stored the site key in .env as a default value. It can easily be overwritten with .env.local when working locally. We also need to store the secret key, and I chose to do it alongside the root config.php, in $config['CLOUDFLARE_TURNSTILE_SECRET'].
@bdebon: I can share the secret key privately, or I can let you create a Turnstile config yourself and use your own values (that's what I'd recommend).
What could be improved?
- Of course, Turnstile is in beta, so it could break. But Cloudflare being huge as it is, I think we can safely use it and update it quickly if any change is announced.
- I don't know which version of PHP is used, so I used features available in 7.0.
- I introduced HTTP response codes and content (in
api.php); which might not be needed, but cannot harm 😉 - For now the turnstile values (
turnstileWidgetIdandturnstileToken) are stored on thewindowobject. That may be a bit rough but I'm not familiar enough with React's best practices. - Also, the tokens are only valid for 300 seconds, so any user waiting more than that between choices will trigger a server-side error (
invalid-token). Not sure if that's something we need to handle? We could refresh the token before posting the choice, but that would add delay that I'd like to avoid. We could also retry in the background with a refreshed token when needed, but that sounds a bit too much IMHO. - Some GitHub checks are skipped or cancelled, but I can figure out why. Any pointers would help 🥇
Thanks 🙏
Just found an issue with my token validation test. Will fix ASAP.
All good now 😉
Hey @quentint, you still have a problem during the build, you can check the ci log for more detail. I'm gonna read the rest of your explanation !
Perfect explanations thank you so much! I will create a cloudflare account for the sake of seeing how all of this work. I think I understand everything about this PR and I could be able to merge and finish the implementation. Right now the CI is not passing because with next, the window object can be undefined sometimes as it can be rendered server side. There is probably a better place to store this key, I have not checked yet.
Yup, I saw that error. Will try to find a better way to store this.