firebase / firebase/firebase-js-sdk
appCheck/recaptcha-error always thrown with ReCaptchaEnterpriseProvider on a correctly-configured Score-type key; consistent 400 on /recaptcha/enterprise/clr
- Dominant language
- TypeScript
- Stars
- 5.1k
- Forks
- 1k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 37
Description
### Operating System
Windows 11
### Environment (if applicable)
Chrome 154, vanilla JS (no framework), Firebase loaded via CDN
### Firebase SDK Version
12.19.0 (also reproduced on 10.13.0)
### Firebase SDK Product(s)
AppCheck
### Project Tooling
Vanilla JS, no bundler — Firebase loaded via CDN script imports from gstatic.com/firebasejs, no npm/package.json
### Detailed Problem Description
Network evidence
POST https://www.google.com/recaptcha/enterprise/clr?k= → consistently 400 Bad Request (response body unreadable from the browser — opaque cross-origin response)
Filtering the Network tab for exchangeRecaptchaEnterpriseToken shows zero requests — Firebase's own exchange endpoint is never called at all, meaning the failure happens entirely within ReCaptchaEnterpriseProvider.getToken(), before Firebase's own backend is ever reached.
reCAPTCHA Enterprise / App Check configuration (all independently verified)
Key type: Website • Score (not Checkbox, not WAF) — confirmed on the key's Edit panel
"Will you use challenges?" toggle: off, per https://firebase.google.com/docs/app-check/recaptcha-keys
Domain allow-list on the key: contains the exact bare hostname (click-with-the-world.vercel.app, no scheme, no trailing slash), confirmed via copy-paste, not visual inspection
reCAPTCHA Enterprise API: enabled, in the same GCP project the Firebase project resolves to
Firebase Console → App Check → Apps: the web app is registered with the reCAPTCHA Enterprise provider, and the site key shown there matches firebase-config.js byte-for-byte
No billing-account requirement applies (confirmed via https://docs.cloud.google.com/recaptcha/docs/billing-information — Essentials tier works without billing)
Only one initializeAppCheck() call exists in the codebase (no duplicate initialization); provider class confirmed as ReCaptchaEnterpriseProvider, not ReCaptchaV3Provider
What else was ruled out
Race condition: originally called getDatabase() immediately after initializeAppCheck() without awaiting a token first. Added an explicit await getToken(appCheck) before touching the database — no change in outcome.
Client-side throttling: _throttleData is in-memory only per Firebase's own source (not persisted), and throws a distinct appCheck/throttled code — different from what we're seeing.
The known /clr 400 "benign telemetry" case: found a near-identical report (https://discuss.google.dev/t/getting-400-error-on-recaptcha-enterprise-clr-automatically-executed-after-obtaining-recaptcha-token/262418) where /clr also 400s but the token from /reload is still valid and usable. That doesn't match our case — here, getToken() actively throws every time.
App Check metrics (Firebase Console) show 0% verified, with a mix of "invalid" and "outdated client" — consistent with the exchange endpoint never being reached at all.
### Steps and code to reproduce issue
import { initializeApp } from "https://www.gstatic.com/firebasejs/12.19.0/firebase-app.js";
import {
initializeAppCheck,
ReCaptchaEnterpriseProvider,
getToken,
} from "https://www.gstatic.com/firebasejs/12.19.0/firebase-app-check.js";
const app = initializeApp(firebaseConfig);
const appCheck = initializeAppCheck(app, {
provider: new ReCaptchaEnterpriseProvider(recaptchaSiteKey),
isTokenAutoRefreshEnabled: true,
});
try {
const result = await getToken(appCheck);
console.log("resolved", result);
} catch (err) {
console.error("threw", { code: err.code, message: err.message, customData: err.customData });
}
getToken(appCheck) rejects every single time, from every browser/device tested:
code: "appCheck/recaptcha-error"
message: "AppCheck: ReCAPTCHA error. (appCheck/recaptcha-error)."
customData: {}
Contributor guide
Research direction
Start with ReCaptchaEnterpriseProvider.getToken() and the getToken(appCheck) call shown in the reproduction. Reproduce the 400 response from /recaptcha/enterprise/clr and confirm whether the Firebase exchangeRecaptchaEnterpriseToken request is still skipped. Done means a correctly configured Score-type key produces a usable App Check token instead of appCheck/recaptcha-error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, typescript
- Domain
- security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100