Root CauseWhat broke, why, and the fix.

Google sign-in worked on one Firebase site and broke on the next

· firebase, auth, api, debugging
ⓘ Operated by TechAthletes. Every post here is a bug we hit in our own work — symptom, root cause, fix. Nothing is sponsored and we are not paid to mention any tool.

Google sign-in was failing on a Firebase Hosting multi-site domain. The site existed, but signInWithPopup stopped with auth/unauthorized-domain.

The default Hosting domains were in Firebase Auth's configuration. The additional site was not.

That distinction was the whole incident. Hosting knew about the site. Auth did not automatically inherit that knowledge.

I could fix it entirely from the command line through the Identity Toolkit Admin API. But the request had two traps: a missing header produced a misleading 403 SERVICE_DISABLED, and the PATCH required the complete domain list.

The second one is the dangerous one. A request that looks like “add this domain” can silently remove the domains that already work.

What I checked first

There were two separate configuration questions: was Google enabled as a sign-in provider, and was the site's domain authorized?

The provider question is a reasonable first suspicion when Google sign-in fails. The API exposes provider configuration at /defaultSupportedIdpConfigs, where google.com should be enabled.

But provider enablement and domain authorization are different settings. Checking one does not answer the other. The error here named the domain: auth/unauthorized-domain.

The wrong assumption was that creating a Hosting site also made it an authorized Auth domain. Both settings belong to the same Firebase project, so it is easy to treat them as one piece of setup.

They are not.

For this failure, the useful comparison was the hostname where signInWithPopup ran against the project's authorizedDomains list. The additional Hosting domain needed its own entry.

Root cause

Firebase Hosting multi-site domains are not automatically added to Firebase Auth's authorized domains.

The Auth configuration contained the default project domains:

<project-id>.firebaseapp.com
<project-id>.web.app

An additional site uses its own hostname, such as <site-a>.web.app. Having that site in Hosting does not insert the hostname into authorizedDomains.

That explains the asymmetry. The default domains were present in the allowlist; the additional domain was missing. A Hosting configuration could therefore be in place while Google sign-in on that hostname still failed.

The fix belonged in the project's Auth configuration. The Identity Toolkit Admin API exposes that configuration here:

https://identitytoolkit.googleapis.com/admin/v2/projects/<project-id>/config

No console visit was required. I needed to read the current configuration, preserve its existing domains, and submit the complete list with the additional Hosting domains included.

The first trap: a misleading 403

A token from gcloud was only part of the request.

With the gcloud user credential used here, leaving out x-goog-user-project caused 403 SERVICE_DISABLED. That error creates another tempting wrong turn: treating it as evidence that the API needs enabling.

The missing piece was the quota-project header.

I used the project ID in both the endpoint and x-goog-user-project. With neutral placeholders substituted for the project and account, the read request was:

TOKEN=$(gcloud auth print-access-token '<account-email>')

curl \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-goog-user-project: <project-id>" \
  "https://identitytoolkit.googleapis.com/admin/v2/projects/<project-id>/config"

Replace the placeholders before running it.

This GET matters for more than confirming the missing hostname. Its current authorizedDomains value is the starting point for the write. Skipping that read is how the next trap becomes dangerous.

The second trap: PATCH means replace here

The update uses:

?updateMask=authorizedDomains

That looks narrow, and it is: the request targets authorizedDomains. But the update mask does not mean “append the values in this request.”

The submitted array replaces the whole list.

If I sent only the new hostname, I would not be adding one authorized domain. I would be setting the authorized domains to a list containing only that hostname. Existing entries omitted from the request would be removed.

That includes the default domains. It also includes any other site domains and localhost that were already present.

For this PATCH, the array is the desired final state. It is not a list of additions.

This is more dangerous than the missing-header error. The missing header stops the request with a 403. An incomplete array can express a valid configuration change while removing entries I intended to keep.

The word PATCH is not enough to tell me how an array behaves. Here, preserving existing entries was part of the fix.

The fix

I included the existing domains and the additional Hosting domains in the same request:

curl -X PATCH \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-goog-user-project: <project-id>" \
  -H "Content-Type: application/json" \
  "https://identitytoolkit.googleapis.com/admin/v2/projects/<project-id>/config?updateMask=authorizedDomains" \
  -d '{
    "authorizedDomains": [
      "<project-id>.firebaseapp.com",
      "<project-id>.web.app",
      "<site-a>.web.app",
      "<site-b>.web.app",
      "localhost"
    ]
  }'

The important part is not this particular set of placeholders. It is that the payload contains every domain that should remain authorized, including the additions.

I would not copy this array into another project's request without reading that project's configuration first. Any existing domain missing from the example still needs to survive the replacement.

The header gets the request through. The complete array keeps the fix from breaking another site.

What I'd check first next time

I would start with the exact hostname and the current authorizedDomains list. A Hosting site being present does not establish that Auth permits sign-in from it.

If provider configuration were in question, I would check whether google.com was enabled separately. For a 403 SERVICE_DISABLED from this API using a gcloud user credential, I would check x-goog-user-project.

Before sending the PATCH, I would compare the proposed array with the existing array. The question is not just “did I include the new domain?” It is also “did I keep every domain that must continue working?”

This came up while opening Google sign-in on PaperLens, one of the sites we run on Firebase Hosting.