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

Open
#10,385 0 comments 0 reactions 0 assignees View on GitHub
api: appcheck needs-attention question
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.