reCAPTCHA
Introduction
Google reCAPTCHA v3 is used in our frontend to help protect sensitive flows from bots and abuse. It runs in the background (no checkbox), produces a score-based token.
Verification happens on the backend. The frontend does not verify the token. Our apps only obtain the token from Google and send it to the API; the backend is responsible for validating it with Google’s reCAPTCHA API.
Current scope: reCAPTCHA is implemented in two projects only — login and account.
Where reCAPTCHA is used
| Project | Flow / screen | Usage |
|---|---|---|
| Account | Change password (request) | Token sent with changePasswordRequest (request code). |
| Account | Change password (form) | Token can be sent with changePassword (submit new password). |
| Account | Change email (request) | Token sent when requesting the code. |
| Account | Change email (form/confirm) | Token sent on submit and on confirm. |
| Account | Verify email | Token sent with verifyEmail (POST account/confirm/email). |
| Account | Setup 2FA | Token sent for generate secret and for verify. |
| Account | Reset password | Provider + widget and useRecaptcha are present; sending recaptcha_token in the API call is currently commented out in src/components/ResetPassword/index.tsx. Error handling still checks for “recaptcha” in the message. |
| Login | Login process | reCAPTCHA is used in the login flow as well. |
| Login | 2FA process | reCAPTCHA is used during the 2FA process. |
| Login | Reset password | reCAPTCHA is used in the reset password flow. |
How it works
- Frontend: The app loads the reCAPTCHA script (via a provider), and an invisible widget gets a token from Google when needed or when we ask for a refresh.
- Frontend: The token is stored (in a hook) and sent to our API as
recaptcha_tokenin the request body. - Backend: The API receives the token and validates it with Google’s reCAPTCHA API. The frontend never performs this verification.
Configuration is done via the site key only, using the env variable NEXT_PUBLIC_RECAPTCHA_SITE_KEY in environment files (e.g. environments/.env.dev, .env.staging, .env.prod). The secret key used for server-side verification stays on the backend.
Tech stack
- Version: reCAPTCHA v3 (invisible, score-based; no user-facing checkbox).
- Library: react-google-recaptcha-v3 (^1.10.1).
Structure
1. The useRecaptcha hook (@/hooks/useRecaptcha)
This hook is the central place for the reCAPTCHA token and refresh behaviour:
captchaToken— The current token string. It is set when the reCAPTCHA widget runs and callsonVerify.setCaptchaToken— Updates the token when a new one is issued; also used to clear the token after a successful submit so the next action gets a fresh one.refreshRecaptcha— A boolean that tells the widget to request a new token. When it is set totrue, the library runs again and callsonVerifywith a new token.setRefreshRecaptcha— Set totruewhen you want to force a token refresh (e.g. after submit, on error, or when the user navigates).
When the token is refreshed:
- On route change: When the pathname changes,
refreshRecaptchais set totrueso each page gets a new token. - On a timer: When a token already exists, an interval runs every 90 seconds and sets
refreshRecaptchatotrue. Tokens last about 2 minutes; refreshing at 90 seconds keeps them valid.
2. UI: Provider and widget
Any flow that needs reCAPTCHA wraps its content in:
GoogleReCaptchaProvider— Loads the reCAPTCHA script. It must receive the site key, e.g.reCaptchaKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY || ""}.GoogleReCaptcha— The invisible widget that:- Runs on mount and whenever
refreshReCaptchabecomestrue, - Calls
onVerify(token)with the new token; containers passsetCaptchaTokenso the hook keeps the latest token, - Does not use a custom
actionin this codebase (same pattern as phpreaction-frontend-login-react).
- Runs on mount and whenever
Containers use GoogleReCaptcha directly from react-google-recaptcha-v3. A thin wrapper RecaptchaToken exists in @/components/recaptcha (same props: onVerify, refreshReCaptcha) for reuse but is not used by the current containers; they use the library components inline.
3. Sending the token to the API
The token is sent as recaptcha_token in the request body. API helpers in @/utils/apiUser.ts accept an optional recaptcha_token in their body types, for example:
changePasswordRequest(tenant, { csrf, recaptcha_token })changePassword(body)wherebodycan includerecaptcha_token- Change-email and 2FA flows also send
recaptcha_tokenin the body.